From af2ac5fcbaf3c6840feced52f55a614f8dca265b Mon Sep 17 00:00:00 2001 From: Gematik Date: Mon, 22 May 2023 18:04:29 +0200 Subject: [PATCH] Release-Candidate 1.11.0-RC5 --- ReleaseNotes.md | 4 + android/build.gradle.kts | 7 +- .../app/debug/ui/DebugSettingsViewModel.kt | 6 +- .../src/main/assets/open_source_licenses.json | 1425 ++++++++--------- .../gematik/ti/erp/app/analytics/Analytics.kt | 207 ++- .../app/analytics/usecase/AnalyticsUseCase.kt | 66 + .../cardunlock/model/UnlockEgkNavigation.kt | 14 +- .../app/cardunlock/ui/UnlockEgKComponents.kt | 3 + .../erp/app/cardunlock/ui/UnlockEgkDialog.kt | 4 +- .../app/cardwall/mini/ui/HealthCardPrompt.kt | 4 +- .../erp/app/cardwall/ui/CardWallAuthDialog.kt | 6 +- .../erp/app/cardwall/ui/CardWallComponents.kt | 3 +- .../erp/app/cardwall/ui/model/Navigation.kt | 24 +- .../de/gematik/ti/erp/app/di/AllModules.kt | 6 +- .../de/gematik/ti/erp/app/di/NetworkModule.kt | 5 + .../erp/app/interceptor/HeadersInterceptor.kt | 9 + .../ui/MainScreenBottomSheetContentState.kt | 42 +- .../app/mainscreen/ui/MainScreenComponents.kt | 69 +- .../ui/MainScreenNavigationScreens.kt | 68 +- .../app/mainscreen/ui/MainScreenSnackbar.kt | 9 +- .../app/onboarding/ui/OnboardingComponents.kt | 10 +- .../ui/HealthCardOrderComponents.kt | 4 +- .../ui/HealthCardOrderState.kt | 6 +- .../ti/erp/app/orders/ui/OrderScreen.kt | 21 +- .../repository/PharmacyLocalDataSource.kt | 9 + .../repository/PharmacyRemoteDataSource.kt | 8 +- .../pharmacy/repository/PharmacyRepository.kt | 17 +- .../gematik/ti/erp/app/pharmacy/ui/Details.kt | 80 +- .../ti/erp/app/pharmacy/ui/MapsOverview.kt | 45 +- .../ti/erp/app/pharmacy/ui/Navigation.kt | 25 +- .../ti/erp/app/pharmacy/ui/OrderOverview.kt | 22 +- .../erp/app/pharmacy/ui/PharmacyOrderState.kt | 10 +- .../app/pharmacy/ui/PharmacySearchOverview.kt | 24 +- .../app/pharmacy/ui/PharmacySearchScreen.kt | 23 +- .../ui/RedeemPrescriptionsController.kt | 119 ++ .../erp/app/pharmacy/ui/model/Navigation.kt | 18 +- .../usecase/PharmacyDirectRedeemUseCase.kt | 21 +- .../pharmacy/usecase/PharmacySearchUseCase.kt | 4 + .../usecase/model/PharmacyUseCaseData.kt | 21 +- .../app/prescription/detail/ui/InfoSheet.kt | 75 +- .../detail/ui/MedicationOverviewScreen.kt | 4 +- .../detail/ui/PrescriptionDetailScreen.kt | 81 +- .../detail/ui/model/Navigation.kt | 32 +- .../app/prescription/ui/MainScreenAvatar.kt | 2 +- .../ui/PrescriptionScreenComponents.kt | 2 +- .../prescription/ui/ScanScreenComponent.kt | 77 +- .../ti/erp/app/prescription/ui/StatusChip.kt | 10 +- .../app/profiles/ui/EditProfileNavigation.kt | 23 +- .../ti/erp/app/redeem/ui/Navigation.kt | 12 +- .../app/redeem/ui/model/RedeemNavigation.kt | 10 +- .../erp/app/settings/ui/SettingsNavigation.kt | 9 +- .../ti/erp/app/settings/ui/SettingsScreen.kt | 4 +- .../app/settings/usecase/SettingsUseCase.kt | 2 +- .../troubleShooting/TroubleshootingContent.kt | 16 +- .../gematik/ti/erp/app/utils/compose/Hints.kt | 2 +- .../main/res/raw/analytics_identifier.json | 487 ++++++ android/src/main/res/raw/nfc_positions.json | 74 + android/src/main/res/values-ar/strings.xml | 66 +- android/src/main/res/values-en/strings.xml | 66 +- android/src/main/res/values-pl/strings.xml | 66 +- android/src/main/res/values-ru/strings.xml | 66 +- android/src/main/res/values-tr/strings.xml | 66 +- android/src/main/res/values-uk/strings.xml | 66 +- android/src/main/res/values/strings.xml | 26 +- .../src/main/res/values/strings_desktop.xml | 2 +- .../app/pharmacy/ui/PharmacyControllerTest.kt | 22 +- .../usecase/PrescriptionUseCaseTest.kt | 4 +- .../de/gematik/ti/erp/app/utils/TestData.kt | 27 + build.gradle.kts | 12 +- common/build.gradle.kts | 2 +- .../ti/erp/app/api/PharmacyRedeemService.kt | 2 +- .../app/fhir/model/CommonRessourceMapper.kt | 4 +- .../erp/app/fhir/model/CommunicationMapper.kt | 3 +- .../erp/app/fhir/model/CommunicationModel.kt | 15 + .../ti/erp/app/fhir/model/KBVMapper.kt | 2 +- .../ti/erp/app/fhir/model/PharmacyMapper.kt | 66 +- .../erp/app/fhir/model/PharmacySearchModel.kt | 6 +- .../repository/InvoiceLocalDataSource.kt | 2 +- .../repository/TaskLocalDataSource.kt | 2 +- .../settings/repository/SettingsRepository.kt | 2 +- .../fhir/model/CommonRessourceMapperTest.kt | 3 +- .../erp/app/fhir/model/PharmacyMapperTest.kt | 60 +- .../direct_redeem_pharmacy_bundle.json | 100 ++ desktop/build.gradle.kts | 2 +- .../gematik/ti/erp/app/common/Splittable.kt | 3 +- .../ti/erp/app/idp/usecase/IdpBasicUseCase.kt | 1 + gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- plugins/dependencies/build.gradle.kts | 2 +- .../gematik/ti/erp/AppDependenciesPlugin.kt | 6 +- plugins/resource-generation/build.gradle.kts | 6 +- rules/build.gradle.kts | 2 +- smartcard-wrapper/build.gradle.kts | 2 +- 93 files changed, 2974 insertions(+), 1202 deletions(-) create mode 100644 android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt create mode 100644 android/src/main/res/raw/analytics_identifier.json create mode 100644 common/src/commonTest/resources/direct_redeem_pharmacy_bundle.json diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 88d4b4a8..8e2581d8 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,7 @@ +# Release 1.11.0-RC5 +- Direct redemption of prescription without TI +- Bugfixes + # Release 1.10.0 - Began implementation for support of private insurances - Bugfixes diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 6bda11c2..d7b13c52 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -60,13 +60,12 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true - - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt index 2ffbc81c..57722637 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt @@ -64,6 +64,7 @@ import java.security.KeyFactory import java.security.Signature import java.time.Instant import java.time.temporal.ChronoUnit +import java.util.* private val HealthCardCert = BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE private val HealthCardCertPrivateKey = BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY @@ -346,11 +347,12 @@ class DebugSettingsViewModel( } } while (obj != null) - pharmacyDirectRedeemUseCase.redeemPrescription( + pharmacyDirectRedeemUseCase.redeemPrescriptionDirectly( url = url, message = message, telematikId = "", - recipientCertificates = certificates + recipientCertificates = certificates, + transactionId = UUID.randomUUID().toString() ).getOrThrow() } diff --git a/android/src/main/assets/open_source_licenses.json b/android/src/main/assets/open_source_licenses.json index 6d12fcf5..6f7cd6b5 100644 --- a/android/src/main/assets/open_source_licenses.json +++ b/android/src/main/assets/open_source_licenses.json @@ -2,7 +2,7 @@ { "project": "Accompanist FlowLayout library", "description": "Utilities for Jetpack Compose", - "version": "0.23.0", + "version": "0.28.0", "developers": [ "Google" ], @@ -14,46 +14,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.accompanist:accompanist-flowlayout:0.23.0" - }, - { - "project": "Accompanist Insets library", - "description": "Utilities for Jetpack Compose", - "version": "0.23.0", - "developers": [ - "Google" - ], - "url": "https://github.com/google/accompanist/", - "year": null, - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.google.accompanist:accompanist-insets:0.23.0" - }, - { - "project": "Accompanist Insets UI library", - "description": "Utilities for Jetpack Compose", - "version": "0.23.0", - "developers": [ - "Google" - ], - "url": "https://github.com/google/accompanist/", - "year": null, - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.google.accompanist:accompanist-insets-ui:0.23.0" + "dependency": "com.google.accompanist:accompanist-flowlayout:0.28.0" }, { "project": "Accompanist Pager Indicators", "description": "Utilities for Jetpack Compose", - "version": "0.23.0", + "version": "0.28.0", "developers": [ "Google" ], @@ -65,12 +31,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.accompanist:accompanist-pager-indicators:0.23.0" + "dependency": "com.google.accompanist:accompanist-pager-indicators:0.28.0" }, { "project": "Accompanist Pager layouts", "description": "Utilities for Jetpack Compose", - "version": "0.23.0", + "version": "0.28.0", "developers": [ "Google" ], @@ -82,12 +48,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.accompanist:accompanist-pager:0.23.0" + "dependency": "com.google.accompanist:accompanist-pager:0.28.0" }, { "project": "Accompanist SwipeRefresh library", "description": "Utilities for Jetpack Compose", - "version": "0.23.0", + "version": "0.28.0", "developers": [ "Google" ], @@ -99,12 +65,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.accompanist:accompanist-swiperefresh:0.23.0" + "dependency": "com.google.accompanist:accompanist-swiperefresh:0.28.0" }, { "project": "Accompanist System UI Controller library", "description": "Utilities for Jetpack Compose", - "version": "0.23.0", + "version": "0.28.0", "developers": [ "Google" ], @@ -116,33 +82,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.accompanist:accompanist-systemuicontroller:0.23.0" - }, - { - "project": "Activity", - "description": "Provides the base Activity subclass and the relevant hooks to build a composable structure on top.", - "version": "1.1.0", - "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0", - "year": "2018", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "androidx.savedstate:savedstate:1.1.0" + "dependency": "com.google.accompanist:accompanist-systemuicontroller:0.28.0" }, { "project": "Activity", "description": "Provides the base Activity subclass and the relevant hooks to build a composable structure on top.", - "version": "1.4.0", + "version": "1.7.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", "year": "2018", "licenses": [ { @@ -150,16 +99,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.activity:activity:1.4.0" + "dependency": "androidx.activity:activity:1.7.0" }, { "project": "Activity Compose", "description": "Compose integration with Activity", - "version": "1.4.0", + "version": "1.7.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", "year": "2020", "licenses": [ { @@ -167,16 +116,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.activity:activity-compose:1.4.0" + "dependency": "androidx.activity:activity-compose:1.7.0" }, { "project": "Activity Kotlin Extensions", "description": "Kotlin extensions for \u0027activity\u0027 artifact", - "version": "1.4.0", + "version": "1.7.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", "year": "2018", "licenses": [ { @@ -184,16 +133,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.activity:activity-ktx:1.4.0" + "dependency": "androidx.activity:activity-ktx:1.7.0" }, { "project": "Android App Startup Runtime", "description": "Android App Startup Runtime", - "version": "1.1.0", + "version": "1.1.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/startup#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/startup#1.1.1", "year": "2020", "licenses": [ { @@ -201,16 +150,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.startup:startup-runtime:1.1.0" + "dependency": "androidx.startup:startup-runtime:1.1.1" }, { "project": "Android AppCompat Library", "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", - "version": "1.4.1", + "version": "1.6.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.4.1", + "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.6.0", "year": "2011", "licenses": [ { @@ -218,16 +167,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.appcompat:appcompat:1.4.1" + "dependency": "androidx.appcompat:appcompat:1.6.0" }, { "project": "Android Arch-Common", "description": "Android Arch-Common", - "version": "2.1.0", + "version": "2.2.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "url": "https://developer.android.com/jetpack/androidx/releases/arch-core#2.2.0", "year": "2017", "licenses": [ { @@ -235,16 +184,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.arch.core:core-common:2.1.0" + "dependency": "androidx.arch.core:core-common:2.2.0" }, { "project": "Android Arch-Runtime", "description": "Android Arch-Runtime", - "version": "2.1.0", + "version": "2.2.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "url": "https://developer.android.com/jetpack/androidx/releases/arch-core#2.2.0", "year": "2017", "licenses": [ { @@ -252,7 +201,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.arch.core:core-runtime:2.1.0" + "dependency": "androidx.arch.core:core-runtime:2.2.0" }, { "project": "Android DataStore", @@ -288,31 +237,14 @@ ], "dependency": "androidx.datastore:datastore-core:1.0.0" }, - { - "project": "Android DB", - "description": "Android DB", - "version": "2.2.0", - "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx/releases/sqlite#2.2.0", - "year": "2017", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "androidx.sqlite:sqlite:2.2.0" - }, { "project": "Android Emoji2 Compat", "description": "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters.", - "version": "1.0.0", + "version": "1.3.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0", + "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", "year": "2017", "licenses": [ { @@ -320,16 +252,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.emoji2:emoji2:1.0.0" + "dependency": "androidx.emoji2:emoji2:1.3.0" }, { "project": "Android Emoji2 Compat view helpers", "description": "View helpers for Emoji2", - "version": "1.0.0", + "version": "1.3.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0", + "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", "year": "2017", "licenses": [ { @@ -337,16 +269,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.emoji2:emoji2-views-helper:1.0.0" + "dependency": "androidx.emoji2:emoji2-views-helper:1.3.0" }, { "project": "Android Lifecycle Kotlin Extensions", "description": "Kotlin extensions for \u0027lifecycle\u0027 artifact", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2019", "licenses": [ { @@ -354,16 +286,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" }, { "project": "Android Lifecycle LiveData", "description": "Android Lifecycle LiveData", - "version": "2.1.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -371,16 +303,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-livedata:2.1.0" + "dependency": "androidx.lifecycle:lifecycle-livedata:2.6.1" }, { "project": "Android Lifecycle LiveData Core", "description": "Android Lifecycle LiveData Core", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -388,16 +320,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-livedata-core:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-livedata-core:2.6.1" }, { "project": "Android Lifecycle Process", "description": "Android Lifecycle Process", - "version": "2.4.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2018", "licenses": [ { @@ -405,16 +337,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-process:2.4.0" + "dependency": "androidx.lifecycle:lifecycle-process:2.6.1" }, { "project": "Android Lifecycle Runtime", "description": "Android Lifecycle Runtime", - "version": "2.4.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -422,16 +354,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-runtime:2.4.0" + "dependency": "androidx.lifecycle:lifecycle-runtime:2.6.1" }, { "project": "Android Lifecycle ViewModel", "description": "Android Lifecycle ViewModel", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -439,16 +371,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-viewmodel:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-viewmodel:2.6.1" }, { "project": "Android Lifecycle ViewModel Kotlin Extensions", "description": "Kotlin extensions for \u0027viewmodel\u0027 artifact", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2018", "licenses": [ { @@ -456,16 +388,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" }, { "project": "Android Lifecycle ViewModel with SavedState", "description": "Android Lifecycle ViewModel", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2018", "licenses": [ { @@ -473,16 +405,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1" }, { "project": "Android Lifecycle-Common", "description": "Android Lifecycle-Common", - "version": "2.4.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -490,16 +422,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-common:2.4.0" + "dependency": "androidx.lifecycle:lifecycle-common:2.6.1" }, { "project": "Android Lifecycle-Common for Java 8 Language", "description": "Android Lifecycle-Common for Java 8 Language", - "version": "2.4.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2017", "licenses": [ { @@ -507,16 +439,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-common-java8:2.4.0" + "dependency": "androidx.lifecycle:lifecycle-common-java8:2.6.1" }, { "project": "Android Navigation Common", "description": "Android Navigation-Common", - "version": "2.4.2", + "version": "2.5.3", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.4.2", + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", "year": "2017", "licenses": [ { @@ -524,16 +456,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.navigation:navigation-common:2.4.2" + "dependency": "androidx.navigation:navigation-common:2.5.3" }, { "project": "Android Navigation Common Kotlin Extensions", "description": "Android Navigation-Common-Ktx", - "version": "2.4.2", + "version": "2.5.3", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.4.2", + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", "year": "2018", "licenses": [ { @@ -541,16 +473,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.navigation:navigation-common-ktx:2.4.2" + "dependency": "androidx.navigation:navigation-common-ktx:2.5.3" }, { "project": "Android Navigation Runtime", "description": "Android Navigation-Runtime", - "version": "2.4.2", + "version": "2.5.3", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.4.2", + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", "year": "2017", "licenses": [ { @@ -558,16 +490,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.navigation:navigation-runtime:2.4.2" + "dependency": "androidx.navigation:navigation-runtime:2.5.3" }, { "project": "Android Navigation Runtime Kotlin Extensions", "description": "Android Navigation-Runtime-Ktx", - "version": "2.4.2", + "version": "2.5.3", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.4.2", + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", "year": "2018", "licenses": [ { @@ -575,16 +507,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.navigation:navigation-runtime-ktx:2.4.2" + "dependency": "androidx.navigation:navigation-runtime-ktx:2.5.3" }, { "project": "Android Paging-Common", "description": "Android Paging-Common", - "version": "3.1.0", + "version": "3.2.0-alpha03", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.2.0-alpha03", "year": "2017", "licenses": [ { @@ -592,16 +524,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.paging:paging-common:3.1.0" + "dependency": "androidx.paging:paging-common:3.2.0-alpha03" }, { "project": "Android Paging-Common Kotlin Extensions", "description": "Kotlin extensions for \u0027paging-common\u0027 artifact", - "version": "3.1.0", + "version": "3.1.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.1.1", "year": "2018", "licenses": [ { @@ -609,16 +541,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.paging:paging-common-ktx:3.1.0" + "dependency": "androidx.paging:paging-common-ktx:3.1.1" }, { "project": "Android Paging-Compose", "description": "Compose integration with Paging", - "version": "1.0.0-alpha14", + "version": "1.0.0-alpha17", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/paging#1.0.0-alpha14", + "url": "https://developer.android.com/jetpack/androidx/releases/paging#1.0.0-alpha17", "year": "2020", "licenses": [ { @@ -626,7 +558,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.paging:paging-compose:1.0.0-alpha14" + "dependency": "androidx.paging:paging-compose:1.0.0-alpha17" }, { "project": "Android Preferences DataStore", @@ -663,47 +595,47 @@ "dependency": "androidx.datastore:datastore-preferences-core:1.0.0" }, { - "project": "Android Resource Inspection - Annotations", - "description": "Annotation processors for Android resource and layout inspection", - "version": "1.0.0", + "project": "Android Preferences KTX", + "description": "Kotlin extensions for preferences", + "version": "1.2.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.0", - "year": "2021", + "url": "https://developer.android.com/jetpack/androidx/releases/preference#1.2.0", + "year": "2018", "licenses": [ { "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.resourceinspection:resourceinspection-annotation:1.0.0" + "dependency": "androidx.preference:preference-ktx:1.2.0" }, { - "project": "Android Resources Library", - "description": "The Resources Library is a static library that you can add to your Android application in order to use resource APIs that backport the latest APIs to older versions of the platform. Compatible on devices running API 14 or later.", - "version": "1.4.1", + "project": "Android Resource Inspection - Annotations", + "description": "Annotation processors for Android resource and layout inspection", + "version": "1.0.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.4.1", - "year": "2019", + "url": "https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.1", + "year": "2021", "licenses": [ { "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.appcompat:appcompat-resources:1.4.1" + "dependency": "androidx.resourceinspection:resourceinspection-annotation:1.0.1" }, { - "project": "Android Room Kotlin Extensions", - "description": "Android Room Kotlin Extensions", - "version": "2.4.1", + "project": "Android Resources Library", + "description": "The Resources Library is a static library that you can add to your Android application in order to use resource APIs that backport the latest APIs to older versions of the platform. Compatible on devices running API 14 or later.", + "version": "1.6.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/room#2.4.1", + "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.6.0", "year": "2019", "licenses": [ { @@ -711,41 +643,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.room:room-ktx:2.4.1" - }, - { - "project": "Android Room-Common", - "description": "Android Room-Common", - "version": "2.4.1", - "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx/releases/room#2.4.1", - "year": "2017", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "androidx.room:room-common:2.4.1" - }, - { - "project": "Android Room-Runtime", - "description": "Android Room-Runtime", - "version": "2.4.1", - "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx/releases/room#2.4.1", - "year": "2017", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "androidx.room:room-runtime:2.4.1" + "dependency": "androidx.appcompat:appcompat-resources:1.6.0" }, { "project": "Android Support AnimatedVectorDrawable", @@ -764,23 +662,6 @@ ], "dependency": "androidx.vectordrawable:vectordrawable-animated:1.1.0" }, - { - "project": "Android Support DynamicAnimation", - "description": "Physics-based animation in support library, where the animations are driven by physics force. You can use this Animation library to create smooth and realistic animations.", - "version": "1.1.0-alpha03", - "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx", - "year": "2017", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03" - }, { "project": "Android Support ExifInterface", "description": "Android Support ExifInterface", @@ -801,11 +682,11 @@ { "project": "Android Support Library Annotations", "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs.", - "version": "1.3.0", + "version": "1.5.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0", + "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.5.0", "year": "2013", "licenses": [ { @@ -813,7 +694,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.annotation:annotation:1.3.0" + "dependency": "androidx.annotation:annotation:1.5.0" }, { "project": "Android Support Library Async Layout Inflater", @@ -852,11 +733,11 @@ { "project": "Android Support Library compat", "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", - "version": "1.7.0", + "version": "1.9.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/core#1.7.0", + "url": "https://developer.android.com/jetpack/androidx/releases/core#1.9.0", "year": "2015", "licenses": [ { @@ -864,7 +745,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.core:core:1.7.0" + "dependency": "androidx.core:core:1.9.0" }, { "project": "Android Support Library Coordinator Layout", @@ -937,11 +818,11 @@ { "project": "Android Support Library Custom View", "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", - "version": "1.0.0", + "version": "1.1.0", "developers": [ "The Android Open Source Project" ], - "url": "http://developer.android.com/tools/extras/support-library.html", + "url": "https://developer.android.com/jetpack/androidx", "year": "2018", "licenses": [ { @@ -949,16 +830,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.customview:customview:1.0.0" + "dependency": "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" }, { "project": "Android Support Library Custom View", "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", - "version": "1.0.0", + "version": "1.1.0", "developers": [ "The Android Open Source Project" ], - "url": "http://developer.android.com/tools/extras/support-library.html", + "url": "https://developer.android.com/jetpack/androidx", "year": "2018", "licenses": [ { @@ -966,7 +847,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + "dependency": "androidx.customview:customview:1.1.0" }, { "project": "Android Support Library Document File", @@ -1106,12 +987,12 @@ }, { "project": "Android Support Library Sliding Pane Layout", - "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", - "version": "1.0.0", + "description": "SlidingPaneLayout offers a responsive, two pane layout that automatically switches between overlapping panes on smaller devices to a side by side view on larger devices.", + "version": "1.2.0", "developers": [ "The Android Open Source Project" ], - "url": "http://developer.android.com/tools/extras/support-library.html", + "url": "https://developer.android.com/jetpack/androidx/releases/slidingpanelayout#1.2.0", "year": "2018", "licenses": [ { @@ -1119,7 +1000,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.slidingpanelayout:slidingpanelayout:1.0.0" + "dependency": "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }, { "project": "Android Support Library v4", @@ -1156,21 +1037,21 @@ "dependency": "androidx.viewpager:viewpager:1.0.0" }, { - "project": "Android Support SQLite - Framework Implementation", - "description": "The implementation of Support SQLite library using the framework code.", - "version": "2.2.0", + "project": "Android Support RecyclerView", + "description": "Android Support RecyclerView", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/sqlite#2.2.0", - "year": "2017", + "url": "https://developer.android.com/jetpack/androidx/releases/recyclerview#1.2.1", + "year": "2014", "licenses": [ { "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.sqlite:sqlite-framework:2.2.0" + "dependency": "androidx.recyclerview:recyclerview:1.2.1" }, { "project": "Android Support VectorDrawable", @@ -1207,21 +1088,38 @@ "dependency": "androidx.tracing:tracing:1.0.0" }, { - "project": "android-database-sqlcipher", - "description": "SQLCipher for Android is a plugin to SQLite that provides full database encryption.", - "version": "4.5.0", + "project": "Android Transition Support Library", + "description": "Android Transition Support Library", + "version": "1.4.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/transition#1.4.1", + "year": "2016", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.transition:transition:1.4.1" + }, + { + "project": "android-maps-utils", + "description": "Handy extensions to the Google Maps Android API.", + "version": "2.2.3", "developers": [ - "Zetetic Support" + "Google Inc." ], - "url": "https://www.zetetic.net/sqlcipher", + "url": "https://github.com/googlemaps/android-maps-utils", "year": null, "licenses": [ { - "license": "", - "license_url": "https://www.zetetic.net/sqlcipher/license/" + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "net.zetetic:android-database-sqlcipher:4.5.0" + "dependency": "com.google.maps.android:android-maps-utils:2.2.3" }, { "project": "AndroidX Autofill", @@ -1243,7 +1141,7 @@ { "project": "AndroidX Futures", "description": "Androidx implementation of Guava\u0027s ListenableFuture", - "version": "1.0.0", + "version": "1.1.0", "developers": [ "The Android Open Source Project" ], @@ -1255,157 +1153,110 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.concurrent:concurrent-futures:1.0.0" + "dependency": "androidx.concurrent:concurrent-futures:1.1.0" }, { - "project": "AndroidX Security", - "description": "AndroidX Security", - "version": "1.1.0-alpha03", + "project": "AndroidX Preference", + "description": "AndroidX Preference", + "version": "1.2.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/security#1.1.0-alpha03", - "year": "2019", + "url": "https://developer.android.com/jetpack/androidx/releases/preference#1.2.0", + "year": "2015", "licenses": [ { "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.security:security-crypto:1.1.0-alpha03" + "dependency": "androidx.preference:preference:1.2.0" }, { - "project": "androidx.profileinstaller:profileinstaller", - "description": "Allows libraries to prepopulate ahead of time compilation traces to be read by ART", - "version": "1.1.0", + "project": "AndroidX Security", + "description": "AndroidX Security", + "version": "1.1.0-alpha04", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/profileinstaller#1.1.0", - "year": "2021", + "url": "https://developer.android.com/jetpack/androidx/releases/security#1.1.0-alpha04", + "year": "2019", "licenses": [ { "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.profileinstaller:profileinstaller:1.1.0" + "dependency": "androidx.security:security-crypto:1.1.0-alpha04" }, { - "project": "Apache Commons Codec", - "description": "The Apache Commons Codec package contains simple encoder and decoders for\n various formats such as Base64 and Hexadecimal. In addition to these\n widely used encoders and decoders, the codec package also maintains a\n collection of phonetic encoding utilities.", - "version": "1.15", + "project": "androidx.customview:poolingcontainer", + "description": "Utilities for listening to the lifecycle of containers that manage their child Views\u0027 lifecycle, such as RecyclerView", + "version": "1.0.0", "developers": [ - "Henri Yandell", - "Tim OBrien", - "Scott Sanders", - "Rodney Waldhoff", - "Daniel Rall", - "Jon S. Stevens", - "Gary Gregory", - "David Graham", - "Julius Davies", - "Thomas Neidhart", - "Rob Tompkins" + "The Android Open Source Project" ], - "url": "https://commons.apache.org/proper/commons-codec/", - "year": "2002", + "url": "https://developer.android.com/jetpack/androidx/releases/customview#1.0.0", + "year": "2021", "licenses": [ { - "license": "Apache License, Version 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "commons-codec:commons-codec:1.15" + "dependency": "androidx.customview:customview-poolingcontainer:1.0.0" }, { - "project": "Apache Commons IO", - "description": "The Apache Commons IO library contains utility classes, stream implementations, file filters,\nfile comparators, endian transformation classes, and much more.", - "version": "2.8.0", + "project": "androidx.profileinstaller:profileinstaller", + "description": "Allows libraries to prepopulate ahead of time compilation traces to be read by ART", + "version": "1.3.0", "developers": [ - "Scott Sanders", - "dIon Gillard", - "Nicola Ken Barozzi", - "Henri Yandell", - "Stephen Colebourne", - "Jeremias Maerki", - "Matthew Hawthorne", - "Martin Cooper", - "Rob Oxspring", - "Jochen Wiedmann", - "Niall Pemberton", - "Jukka Zitting", - "Gary Gregory", - "Kristian Rosenvold" + "The Android Open Source Project" ], - "url": "https://commons.apache.org/proper/commons-io/", - "year": "2002", + "url": "https://developer.android.com/jetpack/androidx/releases/profileinstaller#1.3.0", + "year": "2021", "licenses": [ { - "license": "Apache License, Version 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "commons-io:commons-io:2.8.0" + "dependency": "androidx.profileinstaller:profileinstaller:1.3.0" }, { - "project": "Apache Commons Lang", - "description": "Apache Commons Lang, a package of Java utility classes for the\n classes that are in java.lang\u0027s hierarchy, or are considered to be so\n standard as to justify existence in java.lang.", - "version": "3.12.0", - "developers": [ - "Daniel Rall", - "Stephen Colebourne", - "Henri Yandell", - "Steven Caswell", - "Robert Burrell Donkin", - "Gary D. Gregory", - "Fredrik Westermarck", - "James Carman", - "Niall Pemberton", - "Matt Benson", - "Joerg Schaible", - "Oliver Heger", - "Paul Benedict", - "Benedikt Ritter", - "Duncan Jones", - "Loic Guibert", - "Rob Tompkins" - ], - "url": "https://commons.apache.org/proper/commons-lang/", - "year": "2001", + "project": "app-update", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, "licenses": [ { - "license": "Apache License, Version 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" } ], - "dependency": "org.apache.commons:commons-lang3:3.12.0" + "dependency": "com.google.android.play:app-update:2.0.1" }, { - "project": "Apache Commons Text", - "description": "Apache Commons Text is a library focused on algorithms working on strings.", - "version": "1.9", - "developers": [ - "Bruno P. Kinoshita", - "Benedikt Ritter", - "Rob Tompkins", - "Gary Gregory", - "Duncan Jones" - ], - "url": "https://commons.apache.org/proper/commons-text", - "year": "2014", + "project": "app-update-ktx", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, "licenses": [ { - "license": "Apache License, Version 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" } ], - "dependency": "org.apache.commons:commons-text:1.9" + "dependency": "com.google.android.play:app-update-ktx:2.0.1" }, { "project": "atomicfu", "description": "AtomicFU utilities", - "version": "0.17.0", + "version": "0.18.5", "developers": [ "JetBrains Team" ], @@ -1417,7 +1268,7 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlinx:atomicfu-jvm:0.17.0" + "dependency": "org.jetbrains.kotlinx:atomicfu-jvm:0.18.5" }, { "project": "AutoValue Annotations", @@ -1483,59 +1334,59 @@ }, { "project": "Bouncy Castle ASN.1 Extension and Utility APIs", - "description": "The Bouncy Castle Java APIs for ASN.1 extension and utility APIs used to support bcpkix and bctls. This jar contains APIs for JDK 1.5 to JDK 1.8.", - "version": "1.70", + "description": "The Bouncy Castle Java APIs for ASN.1 extension and utility APIs used to support bcpkix and bctls. This jar contains APIs for JDK 1.8 and up.", + "version": "1.72", "developers": [ "The Legion of the Bouncy Castle Inc." ], - "url": "http://www.bouncycastle.org/java.html", + "url": "https://www.bouncycastle.org/java.html", "year": null, "licenses": [ { "license": "Bouncy Castle Licence", - "license_url": "http://www.bouncycastle.org/licence.html" + "license_url": "https://www.bouncycastle.org/licence.html" } ], - "dependency": "org.bouncycastle:bcutil-jdk15to18:1.70" + "dependency": "org.bouncycastle:bcutil-jdk18on:1.72" }, { "project": "Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs", - "description": "The Bouncy Castle Java APIs for CMS, PKCS, EAC, TSP, CMP, CRMF, OCSP, and certificate generation. This jar contains APIs for JDK 1.5 to JDK 1.8. The APIs can be used in conjunction with a JCE/JCA provider such as the one provided with the Bouncy Castle Cryptography APIs.", - "version": "1.70", + "description": "The Bouncy Castle Java APIs for CMS, PKCS, EAC, TSP, CMP, CRMF, OCSP, and certificate generation. This jar contains APIs for JDK 1.8 and up. The APIs can be used in conjunction with a JCE/JCA provider such as the one provided with the Bouncy Castle Cryptography APIs.", + "version": "1.72", "developers": [ "The Legion of the Bouncy Castle Inc." ], - "url": "http://www.bouncycastle.org/java.html", + "url": "https://www.bouncycastle.org/java.html", "year": null, "licenses": [ { "license": "Bouncy Castle Licence", - "license_url": "http://www.bouncycastle.org/licence.html" + "license_url": "https://www.bouncycastle.org/licence.html" } ], - "dependency": "org.bouncycastle:bcpkix-jdk15to18:1.70" + "dependency": "org.bouncycastle:bcpkix-jdk18on:1.72" }, { "project": "Bouncy Castle Provider", - "description": "The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.5 to JDK 1.8.", - "version": "1.70", + "description": "The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.8 and up.", + "version": "1.72", "developers": [ "The Legion of the Bouncy Castle Inc." ], - "url": "http://www.bouncycastle.org/java.html", + "url": "https://www.bouncycastle.org/java.html", "year": null, "licenses": [ { "license": "Bouncy Castle Licence", - "license_url": "http://www.bouncycastle.org/licence.html" + "license_url": "https://www.bouncycastle.org/licence.html" } ], - "dependency": "org.bouncycastle:bcprov-jdk15to18:1.70" + "dependency": "org.bouncycastle:bcprov-jdk18on:1.72" }, { "project": "C Interop", - "description": "Wrapper for interacting with Realm Kotlin native code. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.0.0\u0027 instead.", - "version": "1.0.0", + "description": "Wrapper for interacting with Realm Kotlin native code. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", "developers": [ "Realm" ], @@ -1547,12 +1398,12 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "io.realm.kotlin:cinterop-android:1.0.0" + "dependency": "io.realm.kotlin:cinterop-android:1.7.1" }, { "project": "CanHub/Android-Image-Cropper", "description": "Image Cropping Library for Android, optimised for Camera / Gallery.", - "version": "4.2.1", + "version": "4.3.2", "developers": [ "CanHub" ], @@ -1564,26 +1415,7 @@ "license_url": "https://api.github.com/licenses/apache-2.0" } ], - "dependency": "com.github.CanHub:Android-Image-Cropper:4.2.1" - }, - { - "project": "Checker Qual", - "description": "Checker Qual is the set of annotations (qualifiers) and supporting classes\n used by the Checker Framework to type check Java source code.\n\n Please\n see artifact:\n org.checkerframework:checker", - "version": "3.8.0", - "developers": [ - "Michael Ernst", - "Werner M. Dietl", - "Suzanne Millstein" - ], - "url": "https://checkerframework.org", - "year": null, - "licenses": [ - { - "license": "The MIT License", - "license_url": "http://opensource.org/licenses/MIT" - } - ], - "dependency": "org.checkerframework:checker-qual:3.8.0" + "dependency": "com.github.CanHub:Android-Image-Cropper:4.3.2" }, { "project": "Collections Kotlin Extensions", @@ -1620,11 +1452,11 @@ { "project": "Compose Animation", "description": "Compose animation library", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.4.2", "year": "2019", "licenses": [ { @@ -1632,16 +1464,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.animation:animation:1.1.0" + "dependency": "androidx.compose.animation:animation:1.4.2" }, { "project": "Compose Animation Core", "description": "Animation engine and animation primitives that are the building blocks of the Compose animation library", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.4.2", "year": "2019", "licenses": [ { @@ -1649,16 +1481,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.animation:animation-core:1.1.0" + "dependency": "androidx.compose.animation:animation-core:1.4.2" }, { "project": "Compose Foundation", "description": "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.2", "year": "2018", "licenses": [ { @@ -1666,16 +1498,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.foundation:foundation:1.1.0" + "dependency": "androidx.compose.foundation:foundation:1.4.2" }, { "project": "Compose Geometry", "description": "Compose classes related to dimensions without units", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2020", "licenses": [ { @@ -1683,16 +1515,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-geometry:1.1.0" + "dependency": "androidx.compose.ui:ui-geometry:1.4.2" }, { "project": "Compose Graphics", "description": "Compose graphics", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2020", "licenses": [ { @@ -1700,16 +1532,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-graphics:1.1.0" + "dependency": "androidx.compose.ui:ui-graphics:1.4.2" }, { "project": "Compose Layouts", "description": "Compose layout implementations", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.2", "year": "2019", "licenses": [ { @@ -1717,16 +1549,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.foundation:foundation-layout:1.1.0" + "dependency": "androidx.compose.foundation:foundation-layout:1.4.2" }, { "project": "Compose Material Components", "description": "Compose Material Design Components library", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", "year": "2018", "licenses": [ { @@ -1734,16 +1566,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.material:material:1.1.0" + "dependency": "androidx.compose.material:material:1.4.2" }, { "project": "Compose Material Icons Core", "description": "Compose Material Design core icons. This module contains the most commonly used set of Material icons.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", "year": "2020", "licenses": [ { @@ -1751,16 +1583,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.material:material-icons-core:1.1.0" + "dependency": "androidx.compose.material:material-icons-core:1.4.2" }, { "project": "Compose Material Icons Extended", "description": "Compose Material Design extended icons. This module contains all Material icons. It is a very large dependency and should not be included directly.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", "year": "2020", "licenses": [ { @@ -1768,16 +1600,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.material:material-icons-extended:1.1.0" + "dependency": "androidx.compose.material:material-icons-extended:1.4.2" }, { "project": "Compose Material Ripple", "description": "Material ripple used to build interactive components", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", "year": "2020", "licenses": [ { @@ -1785,16 +1617,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.material:material-ripple:1.1.0" + "dependency": "androidx.compose.material:material-ripple:1.4.2" }, { "project": "Compose Navigation", "description": "Compose integration with Navigation", - "version": "2.4.2", + "version": "2.5.3", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.4.2", + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", "year": "2020", "licenses": [ { @@ -1802,16 +1634,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.navigation:navigation-compose:2.4.2" + "dependency": "androidx.navigation:navigation-compose:2.5.3" }, { "project": "Compose Runtime", "description": "Tree composition support for code generated by the Compose compiler plugin and corresponding public API", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.4.2", "year": "2019", "licenses": [ { @@ -1819,16 +1651,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.runtime:runtime:1.1.0" + "dependency": "androidx.compose.runtime:runtime:1.4.2" }, { "project": "Compose Saveable", "description": "Compose components that allow saving and restoring the local ui state", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.4.2", "year": "2020", "licenses": [ { @@ -1836,16 +1668,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.runtime:runtime-saveable:1.1.0" + "dependency": "androidx.compose.runtime:runtime-saveable:1.4.2" }, { "project": "Compose Tooling", "description": "Compose tooling library. This library exposes information to our tools for better IDE support.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2019", "licenses": [ { @@ -1853,16 +1685,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-tooling:1.1.0" + "dependency": "androidx.compose.ui:ui-tooling:1.4.2" }, { "project": "Compose Tooling API", "description": "Compose tooling library API. This library provides the API required to declare @Preview composables in user apps.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2021", "licenses": [ { @@ -1870,16 +1702,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-tooling-preview:1.1.0" + "dependency": "androidx.compose.ui:ui-tooling-preview:1.4.2" }, { "project": "Compose Tooling Data", "description": "Compose tooling library data. This library provides data about compose for different tooling purposes.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2021", "licenses": [ { @@ -1887,16 +1719,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-tooling-data:1.1.0" + "dependency": "androidx.compose.ui:ui-tooling-data:1.4.2" }, { "project": "Compose UI primitives", "description": "Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout.", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2019", "licenses": [ { @@ -1904,16 +1736,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui:1.1.0" + "dependency": "androidx.compose.ui:ui:1.4.2" }, { "project": "Compose UI Text", "description": "Compose Text primitives and utilities", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2019", "licenses": [ { @@ -1921,16 +1753,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-text:1.1.0" + "dependency": "androidx.compose.ui:ui-text:1.4.2" }, { "project": "Compose Unit", "description": "Compose classes for simple units", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2020", "licenses": [ { @@ -1938,16 +1770,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-unit:1.1.0" + "dependency": "androidx.compose.ui:ui-unit:1.4.2" }, { "project": "Compose Util", "description": "Internal Compose utilities used by other modules", - "version": "1.1.0", + "version": "1.4.2", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", "year": "2020", "licenses": [ { @@ -1955,33 +1787,28 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.compose.ui:ui-util:1.1.0" + "dependency": "androidx.compose.ui:ui-util:1.4.2" }, { - "project": "Core Kotlin Extensions", - "description": "Kotlin extensions for \u0027core\u0027 artifact", - "version": "1.7.0", + "project": "ContentSquare Android SDK", + "description": "ContentSquareAndroid SDK", + "version": "4.15.0", "developers": [ - "The Android Open Source Project" - ], - "url": "https://developer.android.com/jetpack/androidx/releases/core#1.7.0", - "year": "2018", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } + "ContentSquare" ], - "dependency": "androidx.core:core-ktx:1.7.0" + "url": "https://docs.contentsquare.com/android/", + "year": null, + "licenses": [], + "dependency": "com.contentsquare.android:library:4.15.0" }, { - "project": "Dynamic animation Kotlin Extensions", - "description": "Kotlin extensions for \u0027dynamicanimation\u0027 artifact", - "version": "1.0.0-alpha03", + "project": "Core Kotlin Extensions", + "description": "Kotlin extensions for \u0027core\u0027 artifact", + "version": "1.9.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx", + "url": "https://developer.android.com/jetpack/androidx/releases/core#1.9.0", "year": "2018", "licenses": [ { @@ -1989,31 +1816,31 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03" + "dependency": "androidx.core:core-ktx:1.9.0" }, { - "project": "error-prone annotations", + "project": "core-common", "description": null, - "version": "2.5.1", + "version": "2.0.2", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "Apache 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" } ], - "dependency": "com.google.errorprone:error_prone_annotations:2.5.1" + "dependency": "com.google.android.play:core-common:2.0.2" }, { "project": "Experimental annotation", "description": "Java annotation for use on unstable Android API surfaces. When used in conjunction with the Experimental annotation lint checks, this annotation provides functional parity with Kotlin\u0027s Experimental annotation.", - "version": "1.1.0", + "version": "1.3.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0", "year": "2019", "licenses": [ { @@ -2021,22 +1848,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.annotation:annotation-experimental:1.1.0" - }, - { - "project": "FindBugs-jsr305", - "description": "JSR305 Annotations for Findbugs", - "version": "3.0.2", - "developers": [], - "url": "http://findbugs.sourceforge.net/", - "year": null, - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.google.code.findbugs:jsr305:3.0.2" + "dependency": "androidx.annotation:annotation-experimental:1.3.0" }, { "project": "firebase-annotations", @@ -2116,24 +1928,24 @@ "dependency": "androidx.fragment:fragment-ktx:1.3.6" }, { - "project": "Guava InternalFutureFailureAccess and InternalFutures", - "description": "Contains\n com.google.common.util.concurrent.internal.InternalFutureFailureAccess and\n InternalFutures. Most users will never need to use this artifact. Its\n classes is conceptually a part of Guava, but they\u0027re in this separate\n artifact so that Android libraries can use them without pulling in all of\n Guava (just as they can use ListenableFuture by depending on the\n listenablefuture artifact).", - "version": "1.0.1", + "project": "Gson", + "description": null, + "version": "2.8.9", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "Apache-2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.guava:failureaccess:1.0.1" + "dependency": "com.google.code.gson:gson:2.8.9" }, { "project": "Guava ListenableFuture only", - "description": "An empty artifact that Guava depends on to signal that it is providing\n ListenableFuture -- but is also available in a second \"version\" that\n contains com.google.common.util.concurrent.ListenableFuture class, without\n any other Guava classes. The idea is:\n\n - If users want only ListenableFuture, they depend on listenablefuture-1.0.\n\n - If users want all of Guava, they depend on guava, which, as of Guava\n 27.0, depends on\n listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. The 9999.0-...\n version number is enough for some build systems (notably, Gradle) to select\n that empty artifact over the \"real\" listenablefuture-1.0 -- avoiding a\n conflict with the copy of ListenableFuture in guava itself. If users are\n using an older version of Guava or a build system other than Gradle, they\n may see class conflicts. If so, they can solve them by manually excluding\n the listenablefuture artifact or manually forcing their build systems to\n use 9999.0-....", - "version": "9999.0-empty-to-avoid-conflict-with-guava", + "description": "Contains Guava\u0027s com.google.common.util.concurrent.ListenableFuture class,\n without any of its other classes -- but is also available in a second\n \"version\" that omits the class to avoid conflicts with the copy in Guava\n itself. The idea is:\n\n - If users want only ListenableFuture, they depend on listenablefuture-1.0.\n\n - If users want all of Guava, they depend on guava, which, as of Guava\n 27.0, depends on\n listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. The 9999.0-...\n version number is enough for some build systems (notably, Gradle) to select\n that empty artifact over the \"real\" listenablefuture-1.0 -- avoiding a\n conflict with the copy of ListenableFuture in guava itself. If users are\n using an older version of Guava or a build system other than Gradle, they\n may see class conflicts. If so, they can solve them by manually excluding\n the listenablefuture artifact or manually forcing their build systems to\n use 9999.0-....", + "version": "1.0", "developers": [], "url": null, "year": null, @@ -2143,67 +1955,37 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" + "dependency": "com.google.guava:listenablefuture:1.0" }, { - "project": "Guava: Google Core Libraries for Java", - "description": "Guava is a suite of core and expanded libraries that include\n utility classes, Google\u0027s collections, I/O classes, and\n much more.", - "version": "30.1.1-jre", - "developers": [], - "url": null, - "year": null, - "licenses": [ - { - "license": "Apache License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.google.guava:guava:30.1.1-jre" - }, - { - "project": "HAPI FHIR - Core Library", - "description": null, - "version": "5.5.1", - "developers": [], - "url": "http://jamesagnew.github.io/hapi-fhir/", - "year": null, - "licenses": [ - { - "license": "Apache Software License 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "ca.uhn.hapi.fhir:hapi-fhir-base:5.5.1" - }, - { - "project": "HAPI FHIR Structures - FHIR R4", + "project": "image", "description": null, - "version": "5.5.1", + "version": "1.0.0-beta1", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "Apache Software License 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.5.1" + "dependency": "com.google.android.odml:image:1.0.0-beta1" }, { - "project": "image", + "project": "integrity", "description": null, - "version": "1.0.0-beta1", + "version": "1.1.0", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "Android Software Development Kit License", - "license_url": "https://developer.android.com/studio/terms.html" + "license": "Play Integrity API Terms of Service", + "license_url": "https://developer.android.com/google/play/integrity/overview#tos" } ], - "dependency": "com.google.android.odml:image:1.0.0-beta1" + "dependency": "com.google.android.play:integrity:1.1.0" }, { "project": "IntelliJ IDEA Annotations", @@ -2222,83 +2004,6 @@ ], "dependency": "org.jetbrains:annotations:13.0" }, - { - "project": "J2ObjC Annotations", - "description": "A set of annotations that provide additional information to the J2ObjC\n translator to modify the result of translation.", - "version": "1.3", - "developers": [], - "url": "https://github.com/google/j2objc/", - "year": null, - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.google.j2objc:j2objc-annotations:1.3" - }, - { - "project": "Jackson datatype: JSR310", - "description": "Add-on module to support JSR-310 (Java 8 Date \u0026 Time API) data types.", - "version": "2.12.3", - "developers": [ - "Nick Williams" - ], - "url": null, - "year": null, - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3" - }, - { - "project": "Jackson-annotations", - "description": "Core annotations used for value types, used by Jackson data binding package.", - "version": "2.12.3", - "developers": [], - "url": "http://github.com/FasterXML/jackson", - "year": "2008", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.fasterxml.jackson.core:jackson-annotations:2.12.3" - }, - { - "project": "Jackson-core", - "description": "Core Jackson processing abstractions (aka Streaming API), implementation for JSON", - "version": "2.12.3", - "developers": [], - "url": "https://github.com/FasterXML/jackson-core", - "year": "2008", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.fasterxml.jackson.core:jackson-core:2.12.3" - }, - { - "project": "jackson-databind", - "description": "General data-binding functionality for Jackson: works on core streaming API", - "version": "2.12.3", - "developers": [], - "url": "http://github.com/FasterXML/jackson", - "year": "2008", - "licenses": [ - { - "license": "The Apache Software License, Version 2.0", - "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "com.fasterxml.jackson.core:jackson-databind:2.12.3" - }, { "project": "javax.inject", "description": "The javax.inject API", @@ -2314,29 +2019,14 @@ ], "dependency": "javax.inject:javax.inject:1" }, - { - "project": "JCL 1.2 implemented over SLF4J", - "description": "JCL 1.2 implemented over SLF4J", - "version": "1.7.30", - "developers": [], - "url": "http://www.slf4j.org", - "year": null, - "licenses": [ - { - "license": "Apache License, Version 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - ], - "dependency": "org.slf4j:jcl-over-slf4j:1.7.30" - }, { "project": "Jetpack Camera Core Library", "description": "Core components for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", - "version": "1.1.0-alpha12", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.1.0-alpha12", + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", "year": "2019", "licenses": [ { @@ -2348,16 +2038,16 @@ "license_url": "https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium" } ], - "dependency": "androidx.camera:camera-core:1.1.0-alpha12" + "dependency": "androidx.camera:camera-core:1.2.1" }, { "project": "Jetpack Camera Library Camera2 Implementation/Extensions", "description": "Camera2 implementation and extensions for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", - "version": "1.1.0-alpha12", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.1.0-alpha12", + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", "year": "2019", "licenses": [ { @@ -2365,16 +2055,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.camera:camera-camera2:1.1.0-alpha12" + "dependency": "androidx.camera:camera-camera2:1.2.1" }, { "project": "Jetpack Camera Lifecycle Library", "description": "Lifecycle components for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", - "version": "1.1.0-alpha12", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.1.0-alpha12", + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", "year": "2019", "licenses": [ { @@ -2382,16 +2072,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.camera:camera-lifecycle:1.1.0-alpha12" + "dependency": "androidx.camera:camera-lifecycle:1.2.1" }, { "project": "Jetpack Camera View Library", "description": "UI tools for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", - "version": "1.0.0-alpha32", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-alpha32", + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", "year": "2019", "licenses": [ { @@ -2399,12 +2089,29 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.camera:camera-view:1.0.0-alpha32" + "dependency": "androidx.camera:camera-view:1.2.1" }, { - "project": "JNI Swig Stubs", - "description": "Wrapper for interacting with Realm Kotlin native code from the JVM. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.0.0\u0027 instead.", + "project": "Jetpack WindowManager Library", + "description": "WindowManager Jetpack library. Currently only provides additional functionality on foldable devices.", "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/window#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.window:window:1.0.0" + }, + { + "project": "JNI Swig Stubs", + "description": "Wrapper for interacting with Realm Kotlin native code from the JVM. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", "developers": [ "Realm" ], @@ -2416,12 +2123,12 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "io.realm.kotlin:jni-swig-stub:1.0.0" + "dependency": "io.realm.kotlin:jni-swig-stub:1.7.1" }, { "project": "jose4j", "description": "The jose.4.j library is a robust and easy to use open source implementation of JSON Web Token (JWT) and the JOSE specification suite (JWS, JWE, and JWK).\n It is written in Java and relies solely on the JCA APIs for cryptography.\n Please see https://bitbucket.org/b_c/jose4j/wiki/Home for more info, examples, etc..", - "version": "0.7.12", + "version": "0.9.2", "developers": [ "Brian Campbell" ], @@ -2433,12 +2140,29 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.bitbucket.b_c:jose4j:0.7.12" + "dependency": "org.bitbucket.b_c:jose4j:0.9.2" }, { - "project": "Kodein-DI", - "description": "KODEIN Dependency Injection Core", - "version": "7.11.0", + "project": "kbson", + "description": "KBSON a kotlin multiplatform implementation of the BSON library.", + "version": "0.2.0", + "developers": [ + "" + ], + "url": "http://www.mongodb.org", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.mongodb.kbson:kbson-android:0.2.0" + }, + { + "project": "Kodein", + "description": "Kodein Core", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2450,12 +2174,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-jvm:7.11.0" + "dependency": "org.kodein.di:kodein-di-jvm:7.16.0" }, { - "project": "Kodein-DI-Framework-Android", - "description": "Kodein-DI extensions with AndroidX compatibility", - "version": "7.11.0", + "project": "Kodein-Framework-Android", + "description": "Standard Kodein classes \u0026 extensions for Android", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2467,12 +2191,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-framework-android-x:7.11.0" + "dependency": "org.kodein.di:kodein-di-framework-android-core:7.16.0" }, { - "project": "Kodein-DI-Framework-Android", - "description": "Standard Kodein DI classes \u0026 extensions for Android", - "version": "7.11.0", + "project": "Kodein-Framework-Android", + "description": "Kodein extensions with AndroidX compatibility", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2484,12 +2208,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-framework-android-core:7.11.0" + "dependency": "org.kodein.di:kodein-di-framework-android-x:7.16.0" }, { - "project": "Kodein-DI-Framework-AndroidX-ViewModel", - "description": "Kodein-DI extensions for AndroidX ViewModel", - "version": "7.11.0", + "project": "Kodein-Framework-AndroidX-ViewModel", + "description": "Kodein extensions for AndroidX ViewModel", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2501,12 +2225,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel:7.11.0" + "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel:7.16.0" }, { - "project": "Kodein-DI-Framework-AndroidX-ViewModel-SavedState", - "description": "Kodein-DI extensions for AndroidX ViewModel with SavedStateHandle", - "version": "7.11.0", + "project": "Kodein-Framework-AndroidX-ViewModel-SavedState", + "description": "Kodein extensions for AndroidX ViewModel with SavedStateHandle", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2518,12 +2242,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel-savedstate:7.11.0" + "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel-savedstate:7.16.0" }, { - "project": "Kodein-DI-Framework-Compose", - "description": "Kodein-DI extensions for Jetpack / JetBrains Compose", - "version": "7.11.0", + "project": "Kodein-Framework-Compose", + "description": "Kodein extensions for Jetpack / JetBrains Compose", + "version": "7.16.0", "developers": [ "Kodein Koders" ], @@ -2535,12 +2259,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.di:kodein-di-framework-compose-android:7.11.0" + "dependency": "org.kodein.di:kodein-di-framework-compose-android:7.16.0" }, { "project": "Kodein-Type", "description": "Kodein Type System", - "version": "1.12.0", + "version": "2.2.1", "developers": [ "Kodein Koders" ], @@ -2552,12 +2276,12 @@ "license_url": "https://opensource.org/licenses/MIT" } ], - "dependency": "org.kodein.type:kodein-type-jvm:1.12.0" + "dependency": "org.kodein.type:kaverit-jvm:2.2.1" }, { "project": "Kotlin Android Extensions Runtime", "description": "Kotlin Android Extensions Runtime", - "version": "1.6.10", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2569,12 +2293,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.6.10" + "dependency": "org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.8.10" }, { "project": "Kotlin Reflect", "description": "Kotlin Full Reflection Library", - "version": "1.6.10", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2586,12 +2310,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-reflect:1.6.10" + "dependency": "org.jetbrains.kotlin:kotlin-reflect:1.8.10" }, { "project": "Kotlin Stdlib", "description": "Kotlin Standard Library for JVM", - "version": "1.6.21", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2603,12 +2327,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-stdlib:1.6.21" + "dependency": "org.jetbrains.kotlin:kotlin-stdlib:1.8.10" }, { "project": "Kotlin Stdlib Common", "description": "Kotlin Common Standard Library", - "version": "1.6.21", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2620,12 +2344,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21" + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-common:1.8.10" }, { "project": "Kotlin Stdlib Jdk7", "description": "Kotlin Standard Library JDK 7 extension", - "version": "1.6.21", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2637,12 +2361,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21" + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10" }, { "project": "Kotlin Stdlib Jdk8", "description": "Kotlin Standard Library JDK 8 extension", - "version": "1.6.21", + "version": "1.8.10", "developers": [ "Kotlin Team" ], @@ -2654,12 +2378,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21" + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10" }, { "project": "kotlinx-coroutines-android", "description": "Coroutines support libraries for Kotlin", - "version": "1.6.1", + "version": "1.6.4", "developers": [ "JetBrains Team" ], @@ -2671,12 +2395,29 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1" + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" }, { "project": "kotlinx-coroutines-core", "description": "Coroutines support libraries for Kotlin", - "version": "1.6.1", + "version": "1.6.4", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4" + }, + { + "project": "kotlinx-coroutines-play-services", + "description": "Coroutines support libraries for Kotlin", + "version": "1.6.4", "developers": [ "JetBrains Team" ], @@ -2688,12 +2429,29 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1" + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4" + }, + { + "project": "kotlinx-datetime", + "description": "Kotlin Datetime Library", + "version": "0.4.0", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx-datetime", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.0" }, { "project": "kotlinx-serialization-core", "description": "Kotlin multiplatform serialization runtime library", - "version": "1.3.3", + "version": "1.4.1", "developers": [ "JetBrains Team" ], @@ -2705,12 +2463,12 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.3.3" + "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1" }, { "project": "kotlinx-serialization-json", "description": "Kotlin multiplatform serialization runtime library", - "version": "1.3.3", + "version": "1.4.1", "developers": [ "JetBrains Team" ], @@ -2722,12 +2480,12 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.3.3" + "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1" }, { "project": "Library", - "description": "Library code for Realm Kotlin. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.0.0\u0027 instead.", - "version": "1.0.0", + "description": "Library code for Realm Kotlin. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", "developers": [ "Realm" ], @@ -2739,16 +2497,16 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "io.realm.kotlin:library-base-android:1.0.0" + "dependency": "io.realm.kotlin:library-base-android:1.7.1" }, { "project": "Lifecycle ViewModel Compose", "description": "Compose integration with Lifecycle ViewModel", - "version": "2.4.0", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2021", "licenses": [ { @@ -2756,16 +2514,16 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0" + "dependency": "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" }, { "project": "LiveData Core Kotlin Extensions", "description": "Kotlin extensions for \u0027livedata-core\u0027 artifact", - "version": "2.3.1", + "version": "2.6.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.1", + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", "year": "2018", "licenses": [ { @@ -2773,12 +2531,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.1" + "dependency": "androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.1" }, { "project": "Lottie", "description": "Lottie is an animation library that renders Adobe After Effects animations natively in realtime.", - "version": "5.0.3", + "version": "5.2.0", "developers": [ "Airbnb" ], @@ -2790,12 +2548,12 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0" } ], - "dependency": "com.airbnb.android:lottie:5.0.3" + "dependency": "com.airbnb.android:lottie:5.2.0" }, { "project": "Lottie Compose", "description": "Lottie for Jetpack Compose.", - "version": "5.0.3", + "version": "5.2.0", "developers": [ "Airbnb" ], @@ -2807,7 +2565,58 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0" } ], - "dependency": "com.airbnb.android:lottie-compose:5.0.3" + "dependency": "com.airbnb.android:lottie-compose:5.2.0" + }, + { + "project": "maps-compose", + "description": "Jetpack Compose components for the Maps SDK for Android", + "version": "2.9.1", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-compose", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-compose:2.9.1" + }, + { + "project": "maps-ktx", + "description": "Kotlin extensions (KTX) for Google Maps SDK", + "version": "3.4.0", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-ktx", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-ktx:3.4.0" + }, + { + "project": "maps-utils-ktx", + "description": "Kotlin extensions (KTX) for Google Maps SDK", + "version": "3.4.0", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-ktx", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-utils-ktx:3.4.0" }, { "project": "napier", @@ -2829,7 +2638,7 @@ { "project": "okhttp", "description": "Square’s meticulous HTTP client for Java and Kotlin.", - "version": "4.9.2", + "version": "4.10.0", "developers": [ "Square, Inc." ], @@ -2841,12 +2650,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.squareup.okhttp3:okhttp:4.9.2" + "dependency": "com.squareup.okhttp3:okhttp:4.10.0" }, { "project": "okhttp-logging-interceptor", "description": "Square’s meticulous HTTP client for Java and Kotlin.", - "version": "4.9.2", + "version": "4.10.0", "developers": [ "Square, Inc." ], @@ -2858,12 +2667,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.squareup.okhttp3:logging-interceptor:4.9.2" + "dependency": "com.squareup.okhttp3:logging-interceptor:4.10.0" }, { - "project": "Okio", + "project": "okio", "description": "A modern I/O API for Java", - "version": "2.8.0", + "version": "3.0.0", "developers": [ "Square, Inc." ], @@ -2875,59 +2684,46 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.squareup.okio:okio:2.8.0" + "dependency": "com.squareup.okio:okio-jvm:3.0.0" }, { - "project": "org.hl7.fhir.r4", - "description": null, - "version": "5.4.10", - "developers": [], - "url": null, - "year": null, - "licenses": [ - { - "license": "Apache Software License 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" - } + "project": "Parcelize Runtime", + "description": "Runtime library for the Parcelize compiler plugin", + "version": "1.8.10", + "developers": [ + "Kotlin Team" ], - "dependency": "ca.uhn.hapi.fhir:org.hl7.fhir.r4:5.4.10" - }, - { - "project": "org.hl7.fhir.utilities", - "description": null, - "version": "5.4.10", - "developers": [], - "url": null, + "url": "https://kotlinlang.org/", "year": null, "licenses": [ { - "license": "Apache Software License 2.0", - "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:5.4.10" + "dependency": "org.jetbrains.kotlin:kotlin-parcelize-runtime:1.8.10" }, { - "project": "Parcelize Runtime", - "description": "Runtime library for the Parcelize compiler plugin", - "version": "1.6.10", + "project": "PdfBox-Android", + "description": "The Apache PdfBox project ported to work on Android", + "version": "2.0.27.0", "developers": [ - "Kotlin Team" + "Tom Roush" ], - "url": "https://kotlinlang.org/", + "url": "https://github.com/TomRoush/PdfBox-Android", "year": null, "licenses": [ { - "license": "The Apache License, Version 2.0", + "license": "The Apache Software License, Version 2.0", "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "org.jetbrains.kotlin:kotlin-parcelize-runtime:1.6.10" + "dependency": "com.tom-roush:pdfbox-android:2.0.27.0" }, { "project": "play-services-base", "description": null, - "version": "18.0.1", + "version": "18.1.0", "developers": [], "url": null, "year": null, @@ -2937,12 +2733,12 @@ "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "com.google.android.gms:play-services-base:18.0.1" + "dependency": "com.google.android.gms:play-services-base:18.1.0" }, { "project": "play-services-basement", "description": null, - "version": "18.0.0", + "version": "18.1.0", "developers": [], "url": null, "year": null, @@ -2952,12 +2748,12 @@ "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "com.google.android.gms:play-services-basement:18.0.0" + "dependency": "com.google.android.gms:play-services-basement:18.1.0" }, { "project": "play-services-location", "description": null, - "version": "19.0.1", + "version": "21.0.1", "developers": [], "url": null, "year": null, @@ -2967,42 +2763,42 @@ "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "com.google.android.gms:play-services-location:19.0.1" + "dependency": "com.google.android.gms:play-services-location:21.0.1" }, { - "project": "play-services-mlkit-barcode-scanning", + "project": "play-services-maps", "description": null, - "version": "18.0.0", + "version": "18.1.0", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "ML Kit Terms of Service", - "license_url": "https://developers.google.com/ml-kit/terms" + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0" + "dependency": "com.google.android.gms:play-services-maps:18.1.0" }, { - "project": "play-services-places-placereport", + "project": "play-services-mlkit-barcode-scanning", "description": null, - "version": "17.0.0", + "version": "18.0.0", "developers": [], "url": null, "year": null, "licenses": [ { - "license": "Android Software Development Kit License", - "license_url": "https://developer.android.com/studio/terms.html" + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" } ], - "dependency": "com.google.android.gms:play-services-places-placereport:17.0.0" + "dependency": "com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0" }, { - "project": "play-services-safetynet", + "project": "play-services-tasks", "description": null, - "version": "18.0.1", + "version": "18.0.2", "developers": [], "url": null, "year": null, @@ -3012,22 +2808,24 @@ "license_url": "https://developer.android.com/studio/terms.html" } ], - "dependency": "com.google.android.gms:play-services-safetynet:18.0.1" + "dependency": "com.google.android.gms:play-services-tasks:18.0.2" }, { - "project": "play-services-tasks", - "description": null, - "version": "18.0.1", - "developers": [], - "url": null, - "year": null, + "project": "ReLinker", + "description": "A robust native library loader for Android", + "version": "1.4.5", + "developers": [ + "KeepSafe Software, Inc." + ], + "url": "https://github.com/KeepSafe/ReLinker", + "year": "2015", "licenses": [ { - "license": "Android Software Development Kit License", - "license_url": "https://developer.android.com/studio/terms.html" + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.android.gms:play-services-tasks:18.0.1" + "dependency": "com.getkeepsafe.relinker:relinker:1.4.5" }, { "project": "Retrofit", @@ -3063,14 +2861,61 @@ ], "dependency": "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" }, + { + "project": "review", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:review:2.0.1" + }, + { + "project": "review-ktx", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:review-ktx:2.0.1" + }, + { + "project": "Saved State", + "description": "Android Lifecycle Saved State", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.savedstate:savedstate:1.2.1" + }, { "project": "SavedState Kotlin Extensions", "description": "Kotlin extensions for \u0027savedstate\u0027 artifact", - "version": "1.1.0", + "version": "1.2.1", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0", + "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.1", "year": "2020", "licenses": [ { @@ -3078,12 +2923,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.savedstate:savedstate-ktx:1.1.0" + "dependency": "androidx.savedstate:savedstate-ktx:1.2.1" }, { "project": "SLF4J API Module", "description": "The slf4j API", - "version": "1.7.30", + "version": "1.7.21", "developers": [], "url": "http://www.slf4j.org", "year": null, @@ -3093,12 +2938,12 @@ "license_url": "http://www.opensource.org/licenses/mit-license.php" } ], - "dependency": "org.slf4j:slf4j-api:1.7.30" + "dependency": "org.slf4j:slf4j-api:1.7.21" }, { "project": "Snapper for Jetpack Compose", "description": "Snapper for Jetpack Compose", - "version": "0.1.2", + "version": "0.2.2", "developers": [ "Chris Banes" ], @@ -3110,12 +2955,12 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "dev.chrisbanes.snapper:snapper:0.1.2" + "dependency": "dev.chrisbanes.snapper:snapper:0.2.2" }, { "project": "Tink Cryptography API for Android", "description": "Tink is a small cryptographic library that provides a safe, simple, agile and fast way to accomplish some common cryptographic tasks.", - "version": "1.5.0", + "version": "1.7.0", "developers": [ "" ], @@ -3127,7 +2972,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.crypto.tink:tink-android:1.5.0" + "dependency": "com.google.crypto.tink:tink-android:1.7.0" }, { "project": "transport-api", @@ -3194,7 +3039,7 @@ { "project": "viewbinding", "description": null, - "version": "7.0.0", + "version": "7.2.1", "developers": [], "url": null, "year": null, @@ -3204,7 +3049,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.databinding:viewbinding:7.0.0" + "dependency": "androidx.databinding:viewbinding:7.2.1" }, { "project": "vision-common", @@ -3239,11 +3084,11 @@ { "project": "WebView Support Library", "description": "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions.", - "version": "1.4.0", + "version": "1.6.0", "developers": [ "The Android Open Source Project" ], - "url": "https://developer.android.com/jetpack/androidx/releases/webkit#1.4.0", + "url": "https://developer.android.com/jetpack/androidx/releases/webkit#1.6.0", "year": "2017", "licenses": [ { @@ -3251,7 +3096,7 @@ "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "androidx.webkit:webkit:1.4.0" + "dependency": "androidx.webkit:webkit:1.6.0" }, { "project": "zxcvbn4j", @@ -3273,7 +3118,7 @@ { "project": "ZXing Core", "description": "Core barcode encoding/decoding library", - "version": "3.5.0", + "version": "3.5.1", "developers": [], "url": null, "year": null, @@ -3283,6 +3128,6 @@ "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" } ], - "dependency": "com.google.zxing:core:3.5.0" + "dependency": "com.google.zxing:core:3.5.1" } ] \ No newline at end of file diff --git a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt b/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt index 098f7c3a..238fed18 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt @@ -22,22 +22,32 @@ import android.content.Context import android.content.SharedPreferences import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.content.edit import androidx.navigation.NavHostController import com.contentsquare.android.Contentsquare +import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase +import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCaseData 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.combine private const val PrefsName = "analyticsAllowed" // `gemSpec_eRp_FdV A_20187` class Analytics( private val context: Context, - private val prefs: SharedPreferences + private val prefs: SharedPreferences, + analyticsUseCase: AnalyticsUseCase ) { private val _analyticsAllowed = MutableStateFlow(false) val analyticsAllowed: StateFlow @@ -56,6 +66,32 @@ class Analytics( } } + private val popUpFlow = MutableStateFlow(AnalyticsData.defaultPopUp) + + private var analyticsScreenFlow = combine( + analyticsUseCase.screenNamesFlow, + popUpFlow + ) { + screenNames, popUp -> + AnalyticsData.AnalyticsScreenState(screenNames, popUp) + } + + val screenState + @Composable + get() = analyticsScreenFlow.collectAsState(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, "") + } + } + fun trackScreen(screenName: String) { if (analyticsAllowed.value) { Contentsquare.send(screenName) @@ -113,19 +149,61 @@ class Analytics( } @Composable -fun TrackNavigationChanges(navController: NavHostController) { +fun TrackNavigationChanges( + navController: NavHostController, + previousNavEntry: String, + onNavEntryChange: (String) -> Unit +) { val analytics = LocalAnalytics.current + val analyticsState by analytics.screenState - LaunchedEffect(Unit) { - navController.currentBackStackEntryFlow.collect { - try { - analytics.trackScreen(Uri.parse(it.destination.route).buildUpon().clearQuery().build().toString()) - } catch (expected: Exception) { - Napier.e("Couldn't track navigation screen", expected) + LaunchedEffect(navController.currentBackStackEntry) { + 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 ?: "" + if (name.isNotEmpty()) { + analytics.trackScreen(name) + } else { + analytics.trackScreen(route) } + } catch (expected: Exception) { + Napier.e("Couldn't track navigation screen", expected) } } + +@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) { @@ -152,3 +230,116 @@ 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) + } + } + } +} + +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( + val screenNamesList: List, + var popUp: PopUp + ) + + data class PopUp( + var visible: Boolean, + var name: String + ) + + val defaultPopUp = PopUp( + visible = false, + name = "" + ) + + val defaultAnalyticsState = AnalyticsScreenState( + screenNamesList = emptyList(), + popUp = defaultPopUp + ) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt new file mode 100644 index 00000000..19e87b87 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 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 may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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 android.content.Context +import androidx.compose.runtime.Immutable +import de.gematik.ti.erp.app.R +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.io.InputStream + +class AnalyticsUseCase( + private val context: Context +) { + private val screenNames: List by lazy { + loadAnalyticsScreenNamesFromJSON( + context.resources.openRawResourceFd(R.raw.analytics_identifier).createInputStream() + ) + } + + val screenNamesFlow: Flow> + get() = flow { + emit(screenNames) + } +} + +private fun loadAnalyticsScreenNamesFromJSON( + jsonInput: InputStream +): List = + Json.parseToJsonElement(jsonInput.bufferedReader().readText()).jsonArray.map { + val key = it.jsonObject.keys.first() + print(key) + val name = it.jsonObject.values.first().jsonObject["name"]!!.jsonPrimitive.content + print(name) + AnalyticsUseCaseData.AnalyticsScreenName(key, name) + } + +object AnalyticsUseCaseData { + @Immutable + @Serializable + data class AnalyticsScreenName( + val key: String, + val name: String + ) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt index 26816fb8..6c8129b9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt @@ -21,11 +21,11 @@ package de.gematik.ti.erp.app.cardunlock.model import de.gematik.ti.erp.app.Route object UnlockEgkNavigation { - object Intro : Route("Intro") - object CardAccessNumber : Route("CardAccessNumber") - object PersonalUnblockingKey : Route("PersonalUnblockingKey") - object OldSecret : Route("OldSecret") - object NewSecret : Route("NewSecret") - object UnlockEgk : Route("UnlockEgk") - object TroubleShooting : Route("TroubleShooting") + object Intro : Route("healthCardPassword_introduction") + object CardAccessNumber : Route("healthCardPassword_can") + object PersonalUnblockingKey : Route("healthCardPassword_puk") + object OldSecret : Route("healthCardPassword_oldPin") + object NewSecret : Route("healthCardPassword_pin") + object UnlockEgk : Route("healthCardPassword_readCard") + object TroubleShooting : Route("troubleShooting") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt index 8ff9d841..cc75e614 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt @@ -45,6 +45,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.model.UnlockEgkNavigation import de.gematik.ti.erp.app.cardwall.ui.CardAccessNumber @@ -88,6 +89,8 @@ fun UnlockEgKScreen( var unlockMethod by rememberSaveable { mutableStateOf(unlockMethod) } val unlockNavController = rememberNavController() + var previousNavEntry by remember { mutableStateOf("healthCardPassword_introduction") } + TrackNavigationChanges(unlockNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) var cardAccessNumber by rememberSaveable { mutableStateOf("") } var personalUnblockingKey by rememberSaveable { mutableStateOf("") } var oldSecret by rememberSaveable { mutableStateOf("") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt index f5bb8914..598b2cc2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt @@ -308,12 +308,12 @@ private fun resumeTextFromUnlockEgkState( ) UnlockEgkState.HealthCardCardAccessNumberWrong -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error_alert).toAnnotatedString(), stringResource(R.string.cdw_nfc_intro_step2_info_on_can_error).toAnnotatedString() ) UnlockEgkState.HealthCardPukRetriesLeft -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_puk_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_puk_error_alert).toAnnotatedString(), pukRetriesLeft(state.retriesLeft) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt index 646e5f12..c80c5761 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt @@ -588,12 +588,12 @@ private fun HealthCardErrorDialog( ) HealthCardPromptAuthenticator.State.ReadState.Error.CardAccessNumberWrong -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error_alert).toAnnotatedString(), stringResource(R.string.cdw_nfc_intro_step2_info_on_can_error).toAnnotatedString() ) is HealthCardPromptAuthenticator.State.ReadState.Error.PersonalIdentificationWrong -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error_alert).toAnnotatedString(), pinRetriesLeft(state.retriesLeft) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt index b6ba9731..0a3107e5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt @@ -301,15 +301,15 @@ fun extractRetryText(state: AuthenticationState): Pair Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error_alert).toAnnotatedString(), stringResource(R.string.cdw_nfc_intro_step2_info_on_can_error).toAnnotatedString() ) AuthenticationState.HealthCardPin2RetriesLeft -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error_alert).toAnnotatedString(), pinRetriesLeft(2) ) AuthenticationState.HealthCardPin1RetryLeft -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error_alert).toAnnotatedString(), pinRetriesLeft(1) ) AuthenticationState.HealthCardBlocked -> Pair( diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt index 57ee684c..955f1e5c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt @@ -156,7 +156,8 @@ fun CardWallScreen( } } - TrackNavigationChanges(navController) + var previousNavEntry by remember { mutableStateOf("cardwall_introduction") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) var cardAccessNumber by rememberSaveable { mutableStateOf("") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt index c467c89c..2254cb76 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt @@ -21,17 +21,15 @@ package de.gematik.ti.erp.app.cardwall.ui.model import de.gematik.ti.erp.app.Route object CardWallNavigation { - object Troubleshooting : Route("TroubleShooting") - object ExternalAuthenticator : Route("card_wall_external_authenticator_overview") - object Intro : Route("card_wall_intro") - object MissingCapabilities : Route("card_wall_missing_capabilities") - object CardAccessNumber : Route("card_wall_card_access_number") - - object PersonalIdentificationNumber : Route("card_wall_personal_identification_number") - object AuthenticationSelection : Route("card_wall_authentication_selection") - object AlternativeOption : Route("card_wall_alternative_option") - - object Authentication : Route("card_wall_authentication") - object OrderHealthCard : Route("card_wall_order_health_card") - object UnlockEgk : Route("card_wall_unlock_egk") + object Troubleshooting : Route("troubleShooting") + object ExternalAuthenticator : Route("cardWall_extAuth") + object Intro : Route("cardWall_introduction") + object MissingCapabilities : Route("cardWall_notCapable") + object CardAccessNumber : Route("cardWall_CAN") + object PersonalIdentificationNumber : Route("cardWall_PIN") + object AuthenticationSelection : Route("cardWall_saveLogin") + object AlternativeOption : Route("cardWall_saveLoginSecurityInfo") + object Authentication : Route("cardWall_readCard") + object OrderHealthCard : Route("contactInsuranceCompany") + object UnlockEgk : Route("healthCardPassword_introduction") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt b/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt index 5fbfe37a..d0b730ec 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt @@ -23,6 +23,7 @@ import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase import de.gematik.ti.erp.app.attestation.usecase.integrityModule import de.gematik.ti.erp.app.cardunlock.cardUnlockModule import de.gematik.ti.erp.app.cardwall.cardWallModule @@ -40,6 +41,7 @@ import de.gematik.ti.erp.app.redeem.redeemModule import de.gematik.ti.erp.app.settings.settingsModule import de.gematik.ti.erp.app.vau.vauModule import org.kodein.di.DI +import org.kodein.di.bindProvider import org.kodein.di.bindSingleton import org.kodein.di.instance @@ -80,7 +82,9 @@ val allModules = DI.Module("allModules") { bindSingleton { FeatureToggleManager(instance()) } - bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag)) } + bindProvider { AnalyticsUseCase(instance()) } + + bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag), instance()) } importAll( cardWallModule, diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt b/android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt index 8dafaee3..36fdca75 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt @@ -26,6 +26,7 @@ import de.gematik.ti.erp.app.api.PharmacySearchService import de.gematik.ti.erp.app.idp.api.IdpService import de.gematik.ti.erp.app.interceptor.ApiKeyHeaderInterceptor 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.vau.api.VauService @@ -222,6 +223,10 @@ val networkModule = DI.Module("Network Module") { clientBuilder .addInterceptor(loggingInterceptor) + if (BuildKonfig.INTERNAL) { + clientBuilder.addInterceptor(PharmacyRedeemInterceptor(instance())) + } + Retrofit.Builder() .client(clientBuilder.build()) .baseUrl("https://localhost") // unused but required diff --git a/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt b/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt index c158e4c9..64b9c456 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt @@ -75,6 +75,15 @@ class PharmacySearchInterceptor(private val endpointHelper: EndpointHelper) : In } } +class PharmacyRedeemInterceptor(private val endpointHelper: EndpointHelper) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val original: Request = chain.request() + val request: Request = original.newBuilder() + .build() + return chain.proceed(request) + } +} + class UserAgentHeaderInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt index cb60645d..84e4eb7c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt @@ -76,15 +76,24 @@ import kotlinx.coroutines.launch @Stable sealed class MainScreenBottomSheetContentState { @Stable - object EditProfile : MainScreenBottomSheetContentState() + class EditProfilePicture( + val popUp: MainScreenBottomPopUpNames.EditProfilePicture = MainScreenBottomPopUpNames.EditProfilePicture + ) : MainScreenBottomSheetContentState() @Stable - class EditOrAddProfileName( - val addProfile: Boolean = false + class EditProfileName( + val popUp: MainScreenBottomPopUpNames.EditProfileName = MainScreenBottomPopUpNames.EditProfileName ) : MainScreenBottomSheetContentState() @Stable - object Connect : MainScreenBottomSheetContentState() + class AddProfile( + val popUp: MainScreenBottomPopUpNames.AddProfile = MainScreenBottomPopUpNames.AddProfile + ) : MainScreenBottomSheetContentState() + + @Stable + class Welcome( + val popUp: MainScreenBottomPopUpNames.Welcome = MainScreenBottomPopUpNames.Welcome + ) : MainScreenBottomSheetContentState() } @Composable @@ -99,9 +108,11 @@ fun MainScreenBottomSheetContentState( val profileHandler = LocalProfileHandler.current val title = when (infoContentState) { - MainScreenBottomSheetContentState.EditProfile -> + is MainScreenBottomSheetContentState.EditProfilePicture -> stringResource(R.string.mainscreen_bottom_sheet_edit_profile_image) - is MainScreenBottomSheetContentState.EditOrAddProfileName -> + is MainScreenBottomSheetContentState.EditProfileName -> + stringResource(R.string.bottom_sheet_edit_profile_name_title) + is MainScreenBottomSheetContentState.AddProfile -> stringResource(R.string.bottom_sheet_edit_profile_name_title) else -> null } @@ -128,7 +139,7 @@ fun MainScreenBottomSheetContentState( ) { infoContentState?.let { when (it) { - MainScreenBottomSheetContentState.EditProfile -> + is MainScreenBottomSheetContentState.EditProfilePicture -> EditProfileAvatar( profile = profileHandler.activeProfile, clearPersonalizedImage = { @@ -154,16 +165,21 @@ fun MainScreenBottomSheetContentState( } } ) - is MainScreenBottomSheetContentState.EditOrAddProfileName -> + is MainScreenBottomSheetContentState.EditProfileName -> + ProfileSheetContent( + profilesController = profilesController, + addProfile = false, + profileToEdit = profileToRename, + onCancel = onCancel + ) + is MainScreenBottomSheetContentState.AddProfile -> ProfileSheetContent( profilesController = profilesController, - addProfile = it.addProfile, - profileToEdit = if (!it.addProfile) { - profileToRename - } else { null }, + addProfile = true, + profileToEdit = null, onCancel = onCancel ) - MainScreenBottomSheetContentState.Connect -> + is MainScreenBottomSheetContentState.Welcome -> ConnectBottomSheetContent( onClickConnect = { scope.launch { diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt index 41a25a12..c575a4de 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.mainscreen.ui +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -84,9 +85,13 @@ import de.gematik.ti.erp.app.LegalNoticeWithScaffold import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.TrackPopUps +import de.gematik.ti.erp.app.analytics.trackMainScreenBottomPopUps +import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.ui.CardWallScreen +import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.debug.ui.DebugScreenWrapper import de.gematik.ti.erp.app.license.ui.LicenseScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingNavigationScreens @@ -102,7 +107,6 @@ import de.gematik.ti.erp.app.prescription.ui.MlKitIntroScreen import de.gematik.ti.erp.app.prescription.ui.PrescriptionScreen import de.gematik.ti.erp.app.prescription.ui.ScanScreen import de.gematik.ti.erp.app.prescription.ui.rememberPrescriptionState -import de.gematik.ti.erp.app.prescription.ui.rememberScanPrescriptionController import de.gematik.ti.erp.app.profiles.ui.EditProfileScreen import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.profiles.ui.ProfileImageCropper @@ -142,8 +146,11 @@ fun MainScreen( profilesController: ProfilesController ) { val startDestination = determineStartDestination(settingsController) - - TrackNavigationChanges(navController) + val analytics = LocalAnalytics.current + val analyticsState by analytics.screenState + TrackPopUps(analytics, analyticsState) + var previousNavEntry by remember { mutableStateOf("main") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) NavHost( navController, @@ -174,8 +181,7 @@ fun MainScreen( ) } composable(MainNavigationScreens.Camera.route) { - val scanPrescriptionController = rememberScanPrescriptionController() - ScanScreen(mainNavController = navController, scanPrescriptionController = scanPrescriptionController) + ScanScreen(mainNavController = navController) } composable(MainNavigationScreens.Prescriptions.route) { MainScreenWithScaffold( @@ -244,8 +250,7 @@ fun MainScreen( ) } composable( - MainNavigationScreens.Redeem.route, - MainNavigationScreens.Redeem.arguments + MainNavigationScreens.Redeem.route ) { RedeemNavigation( mainScreenController = mainScreenController, @@ -402,8 +407,8 @@ fun MainScreen( onSaveCroppedImage = { scope.launch { profilesController.savePersonalizedProfileImage(profileId, it) + navController.navigate(MainNavigationScreens.Prescriptions.path()) } - navController.popBackStack() }, onBack = { navController.popBackStack() @@ -462,6 +467,7 @@ private fun determineStartDestination(settingsController: SettingsController) = } @OptIn(ExperimentalMaterialApi::class) +@Suppress("LongMethod") @Composable private fun MainScreenWithScaffold( mainNavController: NavController, @@ -471,27 +477,22 @@ private fun MainScreenWithScaffold( ) { val context = LocalContext.current val bottomNavController = rememberNavController() - val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsState(null) - + var previousNavEntry by remember { mutableStateOf("main") } + TrackNavigationChanges(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val isInPrescriptionScreen by remember { derivedStateOf { currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route } } - CheckInsecureDevice(settingsController, mainNavController) CheckDeviceIntegrity(mainScreenController, mainNavController) - val scaffoldState = rememberScaffoldState() - MainScreenSnackbar( mainScreenController = mainScreenController, scaffoldState = scaffoldState ) - OrderSuccessHandler(mainScreenController) - var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } val sheetState = rememberModalBottomSheetState( @@ -500,11 +501,9 @@ private fun MainScreenWithScaffold( it != ModalBottomSheetValue.HalfExpanded } ) - LaunchedEffect(Unit) { - sheetState.snapTo(ModalBottomSheetValue.Hidden) + sheetState.hide() } - LaunchedEffect(mainScreenBottomSheetContentState) { if (mainScreenBottomSheetContentState != null) { sheetState.show() @@ -512,46 +511,49 @@ private fun MainScreenWithScaffold( 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) + } + } LaunchedEffect(Unit) { if (settingsController.showWelcomeDrawer.first()) { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Connect + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() } } - LaunchedEffect(sheetState.isVisible) { if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { - if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Connect) { + if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Welcome()) { settingsController.welcomeDrawerShown() } mainScreenBottomSheetContentState = null } } - LaunchedEffect(Unit) { if (settingsController.talkbackEnabled(context)) { settingsController.mainScreenTooltipsShown() } } - var profileToRename by remember { mutableStateOf(ProfilesStateData.defaultProfile) } - val toolTipBounds = remember { mutableStateOf>(emptyMap()) } - ToolTips(settingsController, isInPrescriptionScreen, toolTipBounds) - val coroutineScope = rememberCoroutineScope() - BackHandler(enabled = sheetState.isVisible) { coroutineScope.launch { sheetState.hide() } } - ModalBottomSheetLayout( sheetState = sheetState, modifier = Modifier @@ -575,25 +577,22 @@ private fun MainScreenWithScaffold( ) { // TODO: move to general place? ExternalAuthenticationDialog() - MainScreenScaffold( mainScreenController = mainScreenController, settingsController = settingsController, mainNavController = mainNavController, bottomNavController = bottomNavController, tooltipBounds = toolTipBounds, - onClickAddProfile = { mainScreenBottomSheetContentState = - MainScreenBottomSheetContentState.EditOrAddProfileName(addProfile = true) + MainScreenBottomSheetContentState.AddProfile() }, onClickChangeProfileName = { profile -> profileToRename = profile - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditOrAddProfileName() + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfileName() }, - onClickAvatar = { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfile + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfilePicture() }, scaffoldState = scaffoldState ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt index ac149b2e..ea888387 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt @@ -24,6 +24,7 @@ import androidx.navigation.navArgument import kotlinx.serialization.Serializable import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.prescription.detail.ui.model.PopUpName import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.parcelize.Parcelize @@ -32,63 +33,69 @@ import kotlinx.parcelize.Parcelize data class TaskIds(val ids: List) : Parcelable, List by ids object MainNavigationScreens { - object Onboarding : Route("Onboarding") - object Settings : Route("Settings") - object Camera : Route("Camera") - object Prescriptions : Route("Prescriptions") - object Archive : Route("Archive") + object Onboarding : Route("onboarding") + object Settings : Route("settings") + object Camera : Route("main_scanner") + object Prescriptions : Route("main") + object Archive : Route("main_prescriptionArchive") object PrescriptionDetail : Route( - "PrescriptionDetail", + "prescriptionDetail", navArgument("taskId") { type = NavType.StringType } ) { fun path(taskId: String) = path("taskId" to taskId) } - object Orders : Route("Orders") + object Orders : Route("orders") object Messages : Route( - "Messages", + "orders_detail", navArgument("orderId") { type = NavType.StringType } ) { fun path(orderId: String) = Messages.path("orderId" to orderId) } - object Pharmacies : Route("Pharmacies") + object Pharmacies : Route("pharmacySearch") - object Redeem : Route("Redeem") + object Redeem : Route("redeem_methodSelection") - object ProfileImageCropper : Route("ProfileImageCropper", navArgument("profileId") { type = NavType.StringType }) { + object ProfileImageCropper : Route( + "profile_editPicture_imageCropper", + navArgument("profileId") { type = NavType.StringType } + ) { fun path(profileId: String) = path("profileId" to profileId) } object CardWall : Route( - "CardWall", + "cardWall_introduction", navArgument("profileId") { type = NavType.StringType } ) { fun path(profileId: ProfileIdentifier) = path("profileId" to profileId) } - object InsecureDeviceScreen : Route("InsecureDeviceScreen") - object MlKitIntroScreen : Route("MlKitIntroScreen") - object MlKitInformationScreen : Route("MlKitInformationScreen") - object DataProtection : Route("DataProtection") - object IntegrityNotOkScreen : Route("IntegrityInfoScreen") + object InsecureDeviceScreen : Route("main_deviceSecurity") + object MlKitIntroScreen : Route("mlKit") + object MlKitInformationScreen : Route("mlKit_information") + object DataProtection : Route("settings_dataProtection") + object IntegrityNotOkScreen : Route("main_integrityWarning") object EditProfile : - Route("EditProfile", navArgument("profileId") { type = NavType.StringType }) { + Route("profile", navArgument("profileId") { type = NavType.StringType }) { fun path(profileId: String) = path("profileId" to profileId) } - object Terms : Route("Terms") - object Imprint : Route("Imprint") - object OpenSourceLicences : Route("OpenSourceLicences") - object AdditionalLicences : Route("AdditionalLicences") - object AllowAnalytics : Route("AcceptAnalytics") - object Password : Route("Password") - object Debug : Route("Debug") - object OrderHealthCard : Route("OrderHealthCard") + object Terms : Route("settings_termsOfUse") + object Imprint : Route("settings_legalNotice") + object OpenSourceLicences : Route("settings_openSourceLicence") + object AdditionalLicences : Route("settings_additionalLicence") + object AllowAnalytics : Route("settings_productImprovements_complyTracking") + object Password : Route("settings_authenticationMethods_setAppPassword") + object Debug : Route("debug") + object OrderHealthCard : Route("contactInsuranceCompany") - object UnlockEgk : Route("UnlockEgk", navArgument("unlockMethod") { type = NavType.StringType }) { + object UnlockEgk : Route( + "healthCardPassword_introduction", + navArgument("unlockMethod") { type = NavType.StringType } + ) { fun path(unlockMethod: UnlockMethod) = path("unlockMethod" to unlockMethod.name) } } @@ -99,3 +106,10 @@ val MainScreenBottomNavigationItems = listOf( MainNavigationScreens.Pharmacies, MainNavigationScreens.Settings ) + +object MainScreenBottomPopUpNames { + object EditProfilePicture : PopUpName("main_editProfilePicture") + object EditProfileName : PopUpName("main_editName") + object AddProfile : PopUpName("main_createProfile") + object Welcome : PopUpName("main_welcomeDrawer") +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt index 6c913fc1..54078611 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt @@ -32,6 +32,7 @@ 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( @@ -50,8 +51,12 @@ fun MainScreenSnackbar( GeneralErrorState.NetworkNotAvailable -> stringResource(R.string.error_message_network_not_available) is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> - stringResource(R.string.error_message_server_communication_failed).format(it.code) - GeneralErrorState.FatalTruststoreState -> + 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) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt index 368cf1ea..45208ab3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt @@ -105,11 +105,11 @@ import kotlin.math.max import kotlin.math.min object OnboardingNavigationScreens { - object Onboarding : Route("Onboarding") - object Analytics : Route("Analytics") - object TermsOfUse : Route("TermsOfUse") - object DataProtection : Route("DataProtection") - object Biometry : Route("Biometry") + object Onboarding : Route("onboarding") + object Analytics : Route("onboarding_analytics") + object TermsOfUse : Route("onboarding_termsOfUse") + object DataProtection : Route("onboarding_dataProtection") + object Biometry : Route("onboarding_biometry") } private enum class OnboardingPages(val index: Int) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt index 7e361aeb..3c7841ba 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt @@ -82,6 +82,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.AppTheme @@ -102,7 +103,8 @@ fun HealthCardContactOrderScreen( val state by healthCardOrderState.state val navController = rememberNavController() - + var previousNavEntry by remember { mutableStateOf("contactInsuranceCompany") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val title = stringResource(R.string.health_insurance_search_page_title) val navigationMode by navController.navigationModeState(HealthCardOrderNavigationScreens.HealthCardOrder.route) diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt b/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt index f85dbe70..2b30be08 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt @@ -30,9 +30,9 @@ import kotlinx.coroutines.flow.combine import org.kodein.di.compose.rememberInstance object HealthCardOrderNavigationScreens { - object HealthCardOrder : Route("HealthCardOrder") - object SelectOrderOption : Route("SelectOrderOption") - object HealthCardOrderContact : Route("HealthCardOrderContact") + object HealthCardOrder : Route("contactInsuranceCompany") + object SelectOrderOption : Route("contactInsuranceCompany_selectReason") + object HealthCardOrderContact : Route("contactInsuranceCompany_selectMethod") } class HealthCardOrderState( diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt index c4314506..0692594d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.orders.ui +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.MutatePriority @@ -86,6 +87,9 @@ import androidx.compose.ui.unit.em import androidx.navigation.NavController import de.gematik.ti.erp.app.R 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.mainscreen.ui.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold @@ -181,8 +185,21 @@ fun MessageScreen( val sheetState = rememberModalBottomSheetState( ModalBottomSheetValue.Hidden, - confirmStateChange = { it != ModalBottomSheetValue.HalfExpanded } + 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) } @@ -214,7 +231,7 @@ fun MessageScreen( messageState = state, onClickMessage = { selectedMessage = it - scope.launch { sheetState.animateTo(ModalBottomSheetValue.Expanded) } + scope.launch { sheetState.show() } }, onClickPrescription = { mainNavController.navigate( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt index 735f9eb9..f8be321d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt @@ -20,6 +20,7 @@ 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 @@ -125,4 +126,12 @@ class PharmacyLocalDataSource @Inject constructor( isFavorite = true, usageCount = 0 ) + + suspend fun markAsRedeemed(taskId: String) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.apply { + this.redeemedOn = Clock.System.now().toRealmInstant() + } + } + } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt index 7cef566b..f15098b4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRemoteDataSource.kt @@ -59,12 +59,14 @@ class PharmacyRemoteDataSource( } } - suspend fun redeemPrescription( + suspend fun redeemPrescriptionDirectly( url: String, message: ByteArray, pharmacyTelematikId: String, transactionId: String ): Result = safeApiCall("error redeeming prescription with $url") { + val messageBody = message.toRequestBody("application/pkcs7-mime".toMediaType()) + val validatedUrl = url .replace(PlaceholderTelematikId, pharmacyTelematikId, ignoreCase = true) .replace(PlaceholderTransactionId, transactionId, ignoreCase = true) @@ -72,9 +74,7 @@ class PharmacyRemoteDataSource( URL(it) } - val messageBody = message.toRequestBody("application/pkcs7-mime".toMediaType()) - - redeemService.redeem( + redeemService.redeemDirectly( url = validatedUrl.toString(), message = messageBody ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt index 9dc5489f..b6d4e80f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/repository/PharmacyRepository.kt @@ -22,7 +22,7 @@ import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import kotlinx.coroutines.flow.flowOn import de.gematik.ti.erp.app.fhir.model.PharmacyServices -import de.gematik.ti.erp.app.fhir.model.extractBinaryCertificateAsBase64 +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 io.github.aakira.napier.Napier @@ -35,7 +35,6 @@ class PharmacyRepository @Inject constructor( private val localDataSource: PharmacyLocalDataSource, private val dispatchers: DispatchProvider ) { - suspend fun searchPharmacies( names: List, filter: Map @@ -74,26 +73,26 @@ class PharmacyRepository @Inject constructor( } } - suspend fun searchBinaryCert( + suspend fun searchBinaryCerts( locationId: String - ): Result = + ): Result> = withContext(dispatchers.IO) { remoteDataSource.searchBinaryCert( locationId = locationId ).map { - extractBinaryCertificateAsBase64( + extractBinaryCertificatesAsBase64( bundle = it ) } } - suspend fun redeemPrescription( + suspend fun redeemPrescriptionDirectly( url: String, message: ByteArray, pharmacyTelematikId: String, transactionId: String ): Result = - remoteDataSource.redeemPrescription( + remoteDataSource.redeemPrescriptionDirectly( url = url, message = message, pharmacyTelematikId = pharmacyTelematikId, @@ -147,4 +146,8 @@ class PharmacyRepository @Inject constructor( fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = localDataSource.isPharmacyInFavorites(pharmacy).flowOn(dispatchers.IO) + + suspend fun markAsRedeemed(taskId: String) { + localDataSource.markAsRedeemed(taskId) + } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt index 322f8e9a..4ef1f2e6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt @@ -43,7 +43,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -64,9 +63,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarBorder +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -76,11 +77,10 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features import de.gematik.ti.erp.app.fhir.model.Location -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.PickUpPharmacyService import de.gematik.ti.erp.app.fhir.model.isOpenToday import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData @@ -101,6 +101,7 @@ import de.gematik.ti.erp.app.utils.compose.createToastShort 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.flow.first import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone @@ -205,6 +206,7 @@ fun PharmacyDetailsSheetContent( SpacerXXLarge() OrderSelection( + orderState = orderState, pharmacy = pharmacy, onClickOrder = onClickOrderFn ) @@ -220,15 +222,45 @@ private const val NrOfAllOrderOptions = 3 @Composable private fun OrderSelection( pharmacy: PharmacyUseCaseData.Pharmacy, - onClickOrder: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit + onClickOrder: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, + orderState: PharmacyOrderState ) { - if (pharmacy.ready) { - val pickUpService = remember(pharmacy) { pharmacy.provides.any { it is PickUpPharmacyService } } - val deliveryService = remember(pharmacy) { pharmacy.provides.any { it is DeliveryPharmacyService } } - val onlineService = remember(pharmacy) { pharmacy.provides.any { it is OnlinePharmacyService } } + val context = LocalContext.current + val featureToggleManager = FeatureToggleManager(context) + val scope = rememberCoroutineScope() + var directRedeemEnabled by remember { + mutableStateOf(false) + } - val nrOfServices = remember(pickUpService, deliveryService, onlineService) { - listOf(pickUpService, deliveryService, onlineService).count { it } + LaunchedEffect(Unit) { + scope.launch { + directRedeemEnabled = featureToggleManager + .isFeatureEnabled(Features.REDEEM_WITHOUT_TI.featureName).first() && + orderState.profile.lastAuthenticated == null + } + } + + if (pharmacy.ready) { + val directPickUpServiceAvailable = directRedeemEnabled && pharmacy.contacts.pickUpUrl.isNotEmpty() + val pickUpServiceVisible = + pharmacy.pickupServiceAvailable() || directPickUpServiceAvailable + val pickupServiceEnabled = directPickUpServiceAvailable || + !directRedeemEnabled && pharmacy.pickupServiceAvailable() + + val directDeliveryServiceAvailable = directRedeemEnabled && pharmacy.contacts.deliveryUrl.isNotEmpty() + val deliveryServiceVisible = + directDeliveryServiceAvailable || pharmacy.deliveryServiceAvailable() + val deliveryServiceEnabled = directDeliveryServiceAvailable || + !directRedeemEnabled && pharmacy.deliveryServiceAvailable() + + val directOnlineServiceAvailable = directRedeemEnabled && pharmacy.contacts.onlineServiceUrl.isNotEmpty() + val onlineServiceVisible = + pharmacy.onlineServiceAvailable() || directOnlineServiceAvailable + val onlineServiceEnabled = directOnlineServiceAvailable || + !directRedeemEnabled && pharmacy.onlineServiceAvailable() + + val nrOfServices = remember(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) { + listOf(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible).count { it } } val isSingle = nrOfServices == 1 val isLarge = nrOfServices != NrOfAllOrderOptions @@ -238,9 +270,10 @@ private fun OrderSelection( modifier = Modifier.height(IntrinsicSize.Min) ) { val orderModifier = Modifier.weight(weight = 0.5f).fillMaxHeight() - if (pickUpService) { + if (pickUpServiceVisible) { OrderButton( modifier = orderModifier.testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), + enabled = pickupServiceEnabled, onClick = { onClickOrder(pharmacy, PharmacyScreenData.OrderOption.ReserveInPharmacy) }, isLarge = isLarge, text = stringResource(R.string.pharmacy_order_opt_collect), @@ -248,19 +281,21 @@ private fun OrderSelection( ) } - if (deliveryService) { + if (deliveryServiceVisible) { OrderButton( modifier = orderModifier.testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), + enabled = deliveryServiceEnabled, onClick = { onClickOrder(pharmacy, PharmacyScreenData.OrderOption.CourierDelivery) }, isLarge = isLarge, text = stringResource(R.string.pharmacy_order_opt_delivery), image = painterResource(R.drawable.delivery_car_small) ) } - if (onlineService) { + if (onlineServiceVisible) { OrderButton( modifier = orderModifier .testTag(TestTag.PharmacySearch.OrderOptions.OnlineDeliveryOptionButton), + enabled = onlineServiceEnabled, onClick = { onClickOrder(pharmacy, PharmacyScreenData.OrderOption.MailDelivery) }, isLarge = isLarge, text = stringResource(R.string.pharmacy_order_opt_mail), @@ -277,24 +312,39 @@ private fun OrderSelection( } } +@Suppress("MagicNumber") @Composable private fun OrderButton( modifier: Modifier, + enabled: Boolean, isLarge: Boolean = true, text: String, image: Painter, onClick: () -> Unit ) { + val context = LocalContext.current val shape = RoundedCornerShape(16.dp) + val connectText = stringResource(R.string.connect_for_pharmacy_service) Column( modifier = modifier .background(AppTheme.colors.neutral100, shape) .clip(shape) .clickable( role = Role.Button, - onClick = onClick + onClick = { + if (enabled) { + onClick() + } else { + createToastShort(context, connectText) + } + } ) .padding(PaddingDefaults.Medium) + .alpha( + if (enabled) { + 1f + } else { 0.3f } + ) ) { val imgModifier = if (isLarge) { Modifier.align(Alignment.End) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt index 7573ef95..18709800 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt @@ -20,6 +20,7 @@ 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 @@ -86,6 +87,7 @@ 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 @@ -101,10 +103,15 @@ 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.R +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.fhir.model.Location import de.gematik.ti.erp.app.pharmacy.ui.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.detail.ui.model.PopUpName 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 @@ -167,10 +174,15 @@ fun MapsOverviewSmall( sealed interface PharmacySearchSheetContentState { @Stable - data class PharmacySelected(val pharmacy: PharmacyUseCaseData.Pharmacy) : PharmacySearchSheetContentState + data class PharmacySelected( + val pharmacy: PharmacyUseCaseData.Pharmacy, + val popUp: PopUpName = PharmacySearchPopUpNames.PharmacySelected + ) : PharmacySearchSheetContentState @Stable - object FilterSelected : PharmacySearchSheetContentState + data class FilterSelected( + val popUp: PopUpName = PharmacySearchPopUpNames.FilterSelected + ) : PharmacySearchSheetContentState } @OptIn(ExperimentalMaterialApi::class) @@ -178,6 +190,7 @@ sealed interface PharmacySearchSheetContentState { fun MapsOverview( searchController: PharmacySearchController, orderState: PharmacyOrderState, + navController: NavHostController, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, onBack: () -> Unit ) { @@ -229,7 +242,18 @@ fun MapsOverview( 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, @@ -251,7 +275,7 @@ fun MapsOverview( sheetState = sheetState, sheetContent = { when (sheetState.content) { - PharmacySearchSheetContentState.FilterSelected -> + is PharmacySearchSheetContentState.FilterSelected -> FilterSheetContent( modifier = Modifier.navigationBarsPadding(), filter = searchController.searchState.filter, @@ -304,11 +328,14 @@ class PharmacySheetState( 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) { - PharmacySearchSheetContentState.FilterSelected -> ModalBottomSheetValue.Expanded + is PharmacySearchSheetContentState.FilterSelected -> ModalBottomSheetValue.Expanded is PharmacySearchSheetContentState.PharmacySelected -> ModalBottomSheetValue.HalfExpanded } if (snap) { @@ -332,7 +359,7 @@ fun rememberPharmacySheetState( ): PharmacySheetState { val scope = rememberCoroutineScope() val state = rememberSaveable(saver = complexAutoSaver(init = { this.scope = scope })) { - PharmacySheetState(content ?: PharmacySearchSheetContentState.FilterSelected) + PharmacySheetState(content ?: PharmacySearchSheetContentState.FilterSelected()) .apply { this.scope = scope } } LaunchedEffect(content) { @@ -465,7 +492,7 @@ private fun ScaffoldWithMap( } }, onClickFilter = { - onShowBottomSheet(PharmacySearchSheetContentState.FilterSelected) + onShowBottomSheet(PharmacySearchSheetContentState.FilterSelected()) }, onBack = onBack ) @@ -481,7 +508,9 @@ private fun CameraAnimation( onShowSearchButton: () -> Unit ) { var lastMarkerCenter by remember { mutableStateOf(Berlin) } - val isMoving by derivedStateOf { cameraPositionState.isMoving } + val isMoving by remember { + derivedStateOf { cameraPositionState.isMoving } + } val moveDistance = with(LocalDensity.current) { 24.dp.roundToPx() } LaunchedEffect(isMoving) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt index a9cdcf17..11c6e70f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt @@ -21,21 +21,26 @@ 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.compose.ui.platform.LocalContext import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyNavigationScreens import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController 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.flow.first import kotlinx.coroutines.launch @Suppress("LongMethod") @@ -47,11 +52,25 @@ fun PharmacyNavigation( onBack: () -> Unit, onFinish: () -> Unit ) { + val context = LocalContext.current val scope = rememberCoroutineScope() val pharmacySearchController = rememberPharmacySearchController() var searchFilter by remember(pharmacySearchController.searchState.filter) { mutableStateOf(pharmacySearchController.searchState.filter) } + val featureToggleManager = FeatureToggleManager(context) + val hasRedeemableTasks by orderState.hasRedeemableTasks + + LaunchedEffect(Unit) { + searchFilter = searchFilter.copy(directRedeem = false) + val directRedeemEnabled = featureToggleManager + .isFeatureEnabled(Features.REDEEM_WITHOUT_TI.featureName).first() + if (directRedeemEnabled && orderState.profile.lastAuthenticated == null && hasRedeemableTasks + ) { + searchFilter = searchFilter.copy(directRedeem = true) + } + } + var showNoLocationDialog by remember { mutableStateOf(false) } val searchAgainFn = { nearBy: Boolean -> @@ -110,7 +129,8 @@ fun PharmacyNavigation( } } - TrackNavigationChanges(navController) + var previousNavEntry by remember { mutableStateOf("pharmacySearch") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val handleSearchResultFn = { searchResult: PharmacySearchController.SearchQueryResult -> when (searchResult) { @@ -136,6 +156,7 @@ fun PharmacyNavigation( isNestedNavigation = isNestedNavigation, orderState = orderState, onBack = onBack, + navController = navController, onFilterChange = { searchFilter = it }, filter = searchFilter, onStartSearch = { @@ -170,6 +191,7 @@ fun PharmacyNavigation( NavigationAnimation(mode = navigationMode) { PharmacySearchResultScreen( orderState = orderState, + navController = navController, searchController = pharmacySearchController, onBack = { orderState.onResetPharmacySelection() @@ -201,6 +223,7 @@ fun PharmacyNavigation( MapsOverview( searchController = pharmacySearchController, orderState = orderState, + navController = navController, onBack = { orderState.onResetPharmacySelection() navController.popBackStack() diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt index ffc165ce..1bffa620 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt @@ -79,6 +79,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState @@ -89,6 +91,7 @@ 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 kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.UUID @@ -255,16 +258,29 @@ private fun RedeemButton( onClick = { uploadInProgress = true scope.launch { + val featureToggleManager = FeatureToggleManager(context) + val directRedeemEnabled = featureToggleManager + .isFeatureEnabled(Features.REDEEM_WITHOUT_TI.featureName).first() try { - val redeemState = redeemController - .orderPrescriptions( - profileId = orderState.profileId, + val redeemState = if (orderState.profile.lastAuthenticated == null && directRedeemEnabled) { + redeemController.orderPrescriptionsDirectly( orderId = UUID.randomUUID(), prescriptions = order.prescriptions, redeemOption = selectedOrderOption, pharmacy = selectedPharmacy, contact = order.contact ) + } else { + redeemController + .orderPrescriptions( + profileId = orderState.profile.id, + orderId = UUID.randomUUID(), + prescriptions = order.prescriptions, + redeemOption = selectedOrderOption, + pharmacy = selectedPharmacy, + contact = order.contact + ) + } when (redeemState) { is PrescriptionServiceErrorState -> { redeemErrorMessage(context, redeemState)?.let { diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt index 952b97ac..92c07650 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt @@ -31,8 +31,8 @@ import de.gematik.ti.erp.app.core.complexAutoSaver import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData 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.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -45,7 +45,7 @@ import org.kodein.di.compose.rememberInstance @Stable class PharmacyOrderState( - val profileId: ProfileIdentifier, + val profile: ProfilesUseCaseData.Profile, private val useCase: PharmacySearchUseCase, private val scope: CoroutineScope ) { @@ -77,7 +77,7 @@ class PharmacyOrderState( private val prescriptionOrderFlow = useCase - .prescriptionDetailsForOrdering(profileId) + .prescriptionDetailsForOrdering(profile.id) .shareIn(scope, SharingStarted.Lazily, 1) private val hasRedeemableTasksFlow = @@ -135,11 +135,11 @@ fun rememberPharmacyOrderState(): PharmacyOrderState { val useCase by rememberInstance() val dispatchProvider by rememberInstance() return rememberSaveable( - activeProfile.id, + activeProfile, saver = complexAutoSaver() ) { PharmacyOrderState( - activeProfile.id, + activeProfile, useCase, CoroutineScope(dispatchProvider.Default) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt index 36a448a7..f9c8ec72 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt @@ -18,6 +18,7 @@ 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 @@ -52,6 +53,7 @@ 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.mutableStateOf import androidx.compose.runtime.remember @@ -69,6 +71,7 @@ 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 com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import de.gematik.ti.erp.app.theme.AppTheme @@ -76,6 +79,9 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.R 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.pharmacy.model.OverviewPharmacyData import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData @@ -94,6 +100,7 @@ private const val LastUsedPharmaciesListLength = 5 fun PharmacyOverviewScreen( isNestedNavigation: Boolean, orderState: PharmacyOrderState, + navController: NavHostController, onBack: () -> Unit, onStartSearch: () -> Unit, onShowMaps: () -> Unit, @@ -105,6 +112,19 @@ fun PharmacyOverviewScreen( val scope = rememberCoroutineScope() val sheetState = rememberPharmacySheetState() + 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 { AnimatedElevationScaffold( modifier = Modifier.testTag(TestTag.PharmacySearch.OverviewScreen), @@ -124,7 +144,7 @@ fun PharmacyOverviewScreen( onStartSearch = onStartSearch, pharmacySearchController = pharmacySearchController, onShowFilter = { - sheetState.show(PharmacySearchSheetContentState.FilterSelected) + sheetState.show(PharmacySearchSheetContentState.FilterSelected()) }, onShowMaps = onShowMaps ) @@ -134,7 +154,7 @@ fun PharmacyOverviewScreen( sheetState = sheetState, sheetContent = { when (sheetState.content) { - PharmacySearchSheetContentState.FilterSelected -> + is PharmacySearchSheetContentState.FilterSelected -> FilterSheetContent( modifier = Modifier.navigationBarsPadding(), filter = filter, diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt index 22b115ca..ca5b8289 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt @@ -19,6 +19,7 @@ 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 @@ -97,6 +98,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow 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 @@ -104,6 +106,9 @@ import androidx.paging.compose.itemsIndexed import com.google.accompanist.flowlayout.FlowRow import de.gematik.ti.erp.app.R 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.fhir.model.LocalPharmacyService import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData @@ -618,6 +623,7 @@ private fun ErrorRetryHandler( fun PharmacySearchResultScreen( orderState: PharmacyOrderState, searchController: PharmacySearchController, + navController: NavHostController, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, onClickMaps: () -> Unit, onBack: () -> Unit @@ -693,7 +699,18 @@ fun PharmacySearchResultScreen( 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 @@ -756,7 +773,7 @@ fun PharmacySearchResultScreen( }, onClickFilter = { focusManager.clearFocus() - sheetState.show(PharmacySearchSheetContentState.FilterSelected) + sheetState.show(PharmacySearchSheetContentState.FilterSelected()) } ) @@ -775,7 +792,7 @@ fun PharmacySearchResultScreen( sheetState = sheetState, sheetContent = { when (sheetState.content) { - PharmacySearchSheetContentState.FilterSelected -> + is PharmacySearchSheetContentState.FilterSelected -> FilterSheetContent( modifier = Modifier.navigationBarsPadding(), filter = searchFilter, diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt index 6ec661a1..d4e60f8b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt @@ -25,7 +25,10 @@ import de.gematik.ti.erp.app.DispatchProvider 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.pharmacy.ui.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 @@ -44,6 +47,7 @@ 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 @@ -51,6 +55,7 @@ import java.util.UUID @Stable class RedeemPrescriptionsController( private val searchUseCase: PharmacySearchUseCase, + private val pharmacyDirectRedeemUseCase: PharmacyDirectRedeemUseCase, private val overviewUseCase: PharmacyOverviewUseCase, private val dispatchers: DispatchProvider, private val authenticator: Authenticator @@ -61,6 +66,11 @@ class RedeemPrescriptionsController( sealed interface Error : State, PrescriptionServiceErrorState { object Unknown : Error object TaskIdDoesNotExist : Error + object IncorrectDataStructure : Error + object JsonViolated : Error + object Timeout : Error + object Conflict : Error + object Gone : Error } } @@ -81,6 +91,113 @@ class RedeemPrescriptionsController( contact = contact ).cancellable().first() + 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() + + 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.ReserveInPharmacy -> 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.postalCodeAndCity), + 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.ReserveInPharmacy + -> 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 = { + pharmacyDirectRedeemUseCase.markAsRedeemed(order.taskId) + null + }, + onFailure = { + if (it is ApiCallException) { + when (it.response.code()) { + HttpURLConnection.HTTP_BAD_REQUEST -> State.Error.IncorrectDataStructure + HttpURLConnection.HTTP_INTERNAL_ERROR -> State.Error.Unknown + HttpURLConnection.HTTP_UNAUTHORIZED -> State.Error.JsonViolated + HttpURLConnection.HTTP_CLIENT_TIMEOUT -> State.Error.Timeout + HttpURLConnection.HTTP_CONFLICT -> State.Error.Conflict + HttpURLConnection.HTTP_GONE -> State.Error.Gone + + else -> throw it + } + } else { + throw it + } + } + ) + } + }.also { + emit(it) + } + }.map { results -> + State.Ordered(orderId.toString(), results) + }.flowOn(dispatchers.IO) + private fun orderPrescriptionsFlow( profileId: ProfileIdentifier, orderId: UUID, @@ -151,12 +268,14 @@ class RedeemPrescriptionsController( @Composable fun rememberRedeemPrescriptionsController(): RedeemPrescriptionsController { val searchUseCase by rememberInstance() + val pharmacyDirectRedeemUseCase by rememberInstance() val overviewUseCase by rememberInstance() val dispatchers by rememberInstance() val authenticator = LocalAuthenticator.current return remember { RedeemPrescriptionsController( searchUseCase = searchUseCase, + pharmacyDirectRedeemUseCase = pharmacyDirectRedeemUseCase, overviewUseCase = overviewUseCase, dispatchers = dispatchers, authenticator = authenticator diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt index c4bcb31f..ebfd3a97 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt @@ -19,12 +19,18 @@ package de.gematik.ti.erp.app.pharmacy.ui.model import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.prescription.detail.ui.model.PopUpName object PharmacyNavigationScreens { - object StartSearch : Route("pharmacy_start_search") - object List : Route("pharmacy_list") - object Maps : Route("pharmacy_maps") - object OrderOverview : Route("pharmacy_order_overview") - object EditShippingContact : Route("pharmacy_edit_shipping_contact") - object PrescriptionSelection : Route("pharmacy_prescription_selection") + object StartSearch : Route("pharmacySearch") + object List : Route("pharmacySearch_detail") + object Maps : Route("pharmacySearch_map") + object OrderOverview : Route("redeem_viaTI") // TODO change when redeem_viaAVS is available + object EditShippingContact : Route("redeem_editContactInformation") + object PrescriptionSelection : Route("redeem_prescriptionChooseSubset") +} + +object PharmacySearchPopUpNames { + object PharmacySelected : PopUpName("pharmacySearch_selectedPharmacy") + object FilterSelected : PopUpName("pharmacySearch_filter") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt index 634e0356..725ba9e6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyDirectRedeemUseCase.kt @@ -22,24 +22,25 @@ 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 -import java.util.UUID class PharmacyDirectRedeemUseCase( private val repository: PharmacyRepository ) { - suspend fun loadCertificate(locationId: String): Result = + suspend fun loadCertificates(locationId: String): Result> = repository - .searchBinaryCert(locationId = locationId) - .mapCatching { base64Cert -> - X509CertificateHolder(Base64.decode(base64Cert)) + .searchBinaryCerts(locationId = locationId).mapCatching { + list -> + list.map { base64Cert -> + X509CertificateHolder(Base64.decode(base64Cert)) + } } - suspend fun redeemPrescription( + suspend fun redeemPrescriptionDirectly( url: String, message: String, telematikId: String, recipientCertificates: List, - transactionId: String = UUID.randomUUID().toString() + transactionId: String ): Result = runCatching { val asn1Message = buildDirectPharmacyMessage( @@ -47,11 +48,15 @@ class PharmacyDirectRedeemUseCase( recipientCertificates = recipientCertificates ) - repository.redeemPrescription( + repository.redeemPrescriptionDirectly( url = url, message = asn1Message, pharmacyTelematikId = telematikId, transactionId = transactionId ).getOrThrow() } + + suspend fun markAsRedeemed(taskId: String) { + repository.markAsRedeemed(taskId) + } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt index a72a9b32..237918a1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt @@ -69,6 +69,9 @@ class PharmacySearchUseCase( if (locationMode is PharmacyUseCaseData.LocationMode.Enabled) { filterMap += "near" to "${locationMode.location.latitude}|${locationMode.location.longitude}|999|km" } + if (searchData.filter.directRedeem) { + filterMap += "type" to "DELEGATOR" + } if (searchData.filter.ready) { filterMap += "status" to "active" } @@ -262,6 +265,7 @@ class PharmacySearchUseCase( fun List.mapToUseCasePharmacies(): List = map { pharmacy -> PharmacyUseCaseData.Pharmacy( + id = pharmacy.id, name = pharmacy.name, address = pharmacy.address.let { "${it.lines.joinToString()}\n${it.postalCode} ${it.city}" diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt index a75c0481..656b08be 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt @@ -21,10 +21,13 @@ package de.gematik.ti.erp.app.pharmacy.usecase.model import android.os.Parcelable 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.OpeningHours import de.gematik.ti.erp.app.fhir.model.PharmacyContacts import de.gematik.ti.erp.app.fhir.model.Location +import de.gematik.ti.erp.app.fhir.model.OnlinePharmacyService import de.gematik.ti.erp.app.fhir.model.PharmacyService +import de.gematik.ti.erp.app.fhir.model.PickUpPharmacyService import kotlinx.parcelize.Parcelize import kotlinx.datetime.Instant @@ -38,10 +41,11 @@ object PharmacyUseCaseData { val ready: Boolean = false, val deliveryService: Boolean = false, val onlineService: Boolean = false, - val openNow: Boolean = false + val openNow: Boolean = false, + val directRedeem: Boolean = false ) : Parcelable { fun isAnySet(): Boolean = - nearBy || ready || deliveryService || onlineService || openNow + nearBy || ready || deliveryService || onlineService || openNow || directRedeem } /** @@ -49,6 +53,7 @@ object PharmacyUseCaseData { */ @Immutable data class Pharmacy( + val id: String, val name: String, val address: String?, val location: Location?, @@ -67,6 +72,18 @@ object PharmacyUseCaseData { } else { address.replace("\n", ", ") } + + @Stable + fun pickupServiceAvailable(): Boolean = + provides.any { it is PickUpPharmacyService } + + @Stable + fun deliveryServiceAvailable(): Boolean = + provides.any { it is DeliveryPharmacyService } + + @Stable + fun onlineServiceAvailable(): Boolean = + provides.any { it is OnlinePharmacyService } } sealed class LocationMode { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt index a5263caf..c7badeb4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.model.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 @@ -50,17 +51,59 @@ import kotlin.time.Duration.Companion.days sealed class PrescriptionDetailBottomSheetContent { @Stable - class HowLongValid(val prescription: PrescriptionData.Synced) : PrescriptionDetailBottomSheetContent() + class HowLongValid( + val prescription: PrescriptionData.Synced, + val popUp: PrescriptionDetailsPopUpNames.Validity = PrescriptionDetailsPopUpNames.Validity + ) : + PrescriptionDetailBottomSheetContent() - object SubstitutionAllowed : PrescriptionDetailBottomSheetContent() - object DirectAssignment : PrescriptionDetailBottomSheetContent() - object EmergencyFee : PrescriptionDetailBottomSheetContent() - object EmergencyFeeNotExempt : 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() - object AdditionalFeeNotExempt : PrescriptionDetailBottomSheetContent() - object AdditionalFeeExempt : PrescriptionDetailBottomSheetContent() - object Scanned : PrescriptionDetailBottomSheetContent() - object Failure : 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 @@ -68,31 +111,31 @@ fun PrescriptionDetailInfoSheetContent( infoContent: PrescriptionDetailBottomSheetContent ) { when (infoContent) { - PrescriptionDetailBottomSheetContent.DirectAssignment -> + is PrescriptionDetailBottomSheetContent.DirectAssignment -> PrescriptionDetailInfoSheetContent( title = stringResource(R.string.pres_details_exp_da_title), info = stringResource(R.string.pres_details_exp_da_info) ) - PrescriptionDetailBottomSheetContent.EmergencyFee -> + is PrescriptionDetailBottomSheetContent.EmergencyFee -> PrescriptionDetailInfoSheetContent( title = stringResource(R.string.pres_details_exp_em_fee_title), info = stringResource(R.string.pres_details_exp_em_fee_info) ) - PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt -> + 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) ) - PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt -> + is PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt -> PrescriptionDetailInfoSheetContent( title = stringResource(R.string.pres_details_exp_add_fee_title), info = stringResource(R.string.pres_details_exp_add_fee_info) ) - PrescriptionDetailBottomSheetContent.AdditionalFeeExempt -> + 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) @@ -132,7 +175,7 @@ fun PrescriptionDetailInfoSheetContent( } } - PrescriptionDetailBottomSheetContent.SubstitutionAllowed -> + is PrescriptionDetailBottomSheetContent.SubstitutionAllowed -> PrescriptionDetailInfoSheetContent( title = stringResource(R.string.pres_details_exp_sub_allowed_title), info = stringResource(R.string.pres_details_exp_sub_allowed_info) @@ -144,7 +187,7 @@ fun PrescriptionDetailInfoSheetContent( info = stringResource(R.string.pres_details_exp_scanned_info) ) - PrescriptionDetailBottomSheetContent.Failure -> + is PrescriptionDetailBottomSheetContent.Failure -> PrescriptionDetailInfoSheetContent( title = stringResource(R.string.pres_details_exp_failure_title), info = stringResource(R.string.pres_details_exp_failure_info) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt index c5e0ccf0..61110f37 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt @@ -79,7 +79,7 @@ fun MedicationOverviewScreen( ) SpacerMedium() Label( - text = med.text, + text = med.name(), label = null, onClick = { onClickMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) @@ -101,7 +101,7 @@ fun MedicationOverviewScreen( dispense.medication?.let { item { Label( - text = it.text, + text = it.name(), label = null, onClick = { onClickMedication(PrescriptionData.Medication.Dispense(dispense)) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt index 4dc5ca38..a1bf8b40 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt @@ -20,6 +20,7 @@ package de.gematik.ti.erp.app.prescription.detail.ui +import android.net.Uri import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -81,8 +82,13 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +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.core.LocalAuthenticator import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsNavigationScreens @@ -133,13 +139,13 @@ fun PrescriptionDetailsScreen( val mainScope = rememberCoroutineScope { Dispatchers.Main } val onBack: () -> Unit = { mainScope.launch { - mainNavController.popBackStack() + mainNavController.popBackStack() // TODO onBack instead of NavController } } - + val navController = rememberNavController() + var previousNavEntry by remember { mutableStateOf("prescriptionDetail") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) prescription?.let { pres -> - - val navController = rememberNotSaveableNavController() NavHost( navController = navController, startDestination = PrescriptionDetailsNavigationScreens.Overview.route @@ -247,6 +253,18 @@ private fun PrescriptionDetailsWithScaffold( 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() @@ -472,7 +490,10 @@ private fun SyncedPrescriptionOverview( Label( text = text, label = stringResource(R.string.pres_details_additional_fee), - onClick = onClickAdditionalFee(prescription.medicationRequest.additionalFee, onShowInfo) + onClick = onClickAdditionalFee( + prescription.medicationRequest.additionalFee, + onShowInfo + ) ) } @@ -588,9 +609,13 @@ private fun onClickEmergencyFee( onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit ): () -> Unit = { if (emergencyFee) { - onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt) + onShowInfo( + PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt() + ) } else { - onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFee) + onShowInfo( + PrescriptionDetailBottomSheetContent.EmergencyFee() + ) } } @@ -609,10 +634,16 @@ private fun onClickAdditionalFee( onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit ): () -> Unit = { when (additionalFee) { - SyncedTaskData.AdditionalFee.NotExempt -> - onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt) - SyncedTaskData.AdditionalFee.Exempt -> - onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeExempt) + SyncedTaskData.AdditionalFee.NotExempt -> { + onShowInfo( + PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt() + ) + } + SyncedTaskData.AdditionalFee.Exempt -> { + onShowInfo( + PrescriptionDetailBottomSheetContent.AdditionalFeeExempt() + ) + } else -> {} } } @@ -696,21 +727,31 @@ fun SyncedHeader( prescription.isIncomplete -> { SpacerShortMedium() FailureDetailsStatusChip( - onClick = { onShowInfo(PrescriptionDetailBottomSheetContent.Failure) } + onClick = { + onShowInfo(PrescriptionDetailBottomSheetContent.Failure()) + } ) } prescription.isDirectAssignment -> { SpacerShortMedium() DirectAssignmentChip( - onClick = { onShowInfo(PrescriptionDetailBottomSheetContent.DirectAssignment) } + onClick = { + onShowInfo( + PrescriptionDetailBottomSheetContent.DirectAssignment() + ) + } ) } prescription.isSubstitutionAllowed -> { SpacerShortMedium() SubstitutionAllowedChip( - onClick = { onShowInfo(PrescriptionDetailBottomSheetContent.SubstitutionAllowed) } + onClick = { + onShowInfo( + PrescriptionDetailBottomSheetContent.SubstitutionAllowed() + ) + } ) } } @@ -723,7 +764,13 @@ fun SyncedHeader( prescription.state is SyncedTaskData.SyncedTask.Ready || prescription.state is SyncedTaskData.SyncedTask.LaterRedeemable ) -> { - { onShowInfo(PrescriptionDetailBottomSheetContent.HowLongValid(prescription)) } + { + onShowInfo( + PrescriptionDetailBottomSheetContent.HowLongValid( + prescription + ) + ) + } } else -> null @@ -808,7 +855,9 @@ private fun ScannedPrescriptionOverview( textAlign = TextAlign.Center ) SpacerShortMedium() - ScannedChip(onClick = { onShowInfo(PrescriptionDetailBottomSheetContent.Scanned) }) + ScannedChip(onClick = { + onShowInfo(PrescriptionDetailBottomSheetContent.Scanned()) + }) SpacerShortMedium() val date = dateWithIntroductionString(R.string.prs_low_detail_scanned_on, prescription.scannedOn) Text(date, style = AppTheme.typography.body2l) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt index 37e0f27b..b3223e72 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt @@ -18,16 +18,30 @@ package de.gematik.ti.erp.app.prescription.detail.ui.model +import androidx.compose.runtime.Immutable import de.gematik.ti.erp.app.Route object PrescriptionDetailsNavigationScreens { - object Overview : Route("overview") - object MedicationOverview : Route("medicationOverview") - object Medication : Route("medication") - object Patient : Route("patient") - object Prescriber : Route("prescriber") - object Organization : Route("organization") - object Accident : Route("accident") - object TechnicalInformation : Route("technicalInformation") - object Ingredient : Route("ingredient") + object Overview : Route("prescriptionDetail") + object MedicationOverview : Route("prescriptionDetail_medicationOverview") + object Medication : Route("prescriptionDetail_medication") + object Patient : Route("prescriptionDetail_patient") + object Prescriber : Route("prescriptionDetail_practitioner") + object Organization : Route("prescriptionDetail_organization") + object Accident : Route("prescriptionDetail_accidentInfo") + object TechnicalInformation : Route("prescriptionDetail_technicalInfo") + object Ingredient : Route("prescriptionDetail_medication_ingredients") } + +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") +} + +@Immutable +open class PopUpName(val name: String) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt index e5d6929d..e53e8f8f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt @@ -215,7 +215,7 @@ fun ConnectionHelper(onClickRefresh: () -> Unit) { val profile = profileHandler.activeProfile val ssoTokenScope = profile.ssoTokenScope - if (profile.lastAuthenticated != null && ssoTokenScope?.token == null) { + if (ssoTokenScope?.token == null) { TertiaryButton(onClickRefresh) { Text(stringResource(R.string.mainscreen_login)) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt index d68ad13d..b483ac60 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt @@ -590,7 +590,7 @@ fun FullDetailMedication( if (prescription.isIncomplete) { FailureStatusChip() } else if (showDirectAssignmentLabel) { - DirectAssignmentStatusChip() + DirectAssignmentStatusChip(prescription.redeemedOn != null) } else { when (prescription.state) { is SyncedTaskData.SyncedTask.InProgress -> InProgressStatusChip() diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt index 46ee6221..9ebc5b9e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt @@ -25,6 +25,7 @@ import android.content.Context.VIBRATOR_SERVICE import android.content.pm.PackageManager import android.media.AudioManager import android.media.ToneGenerator +import android.net.Uri import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.VibrationEffect.createOneShot @@ -122,8 +123,13 @@ import androidx.core.content.ContextCompat import androidx.navigation.NavController import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.produceState import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.analytics.trackScannerPopUps +import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens import de.gematik.ti.erp.app.prescription.ui.model.ScanData import de.gematik.ti.erp.app.theme.AppTheme @@ -136,6 +142,7 @@ import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringBold import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale @@ -144,9 +151,10 @@ import java.util.concurrent.Executors @OptIn(ExperimentalMaterialApi::class) @Composable fun ScanScreen( - mainNavController: NavController, - scanPrescriptionController: ScanPrescriptionController + mainNavController: NavController ) { + val scanPrescriptionController = rememberScanPrescriptionController() + val context = LocalContext.current var camPermissionGranted by rememberSaveable { mutableStateOf(false) } @@ -167,7 +175,21 @@ fun ScanScreen( var flashEnabled by remember { mutableStateOf(false) } val state by scanPrescriptionController.state + val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + val analytics = LocalAnalytics.current + val analyticsState by analytics.screenState + LaunchedEffect(sheetState.isVisible) { + if (sheetState.isVisible) { + analytics.trackScannerPopUps() + } else { + analytics.onPopUpClosed() + val route = Uri.parse(mainNavController.currentBackStackEntry!!.destination.route) + .buildUpon().clearQuery().build().toString() + trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + } + } + val coroutineScope = rememberCoroutineScope() var cancelRequested by remember { mutableStateOf(false) } @@ -177,6 +199,12 @@ fun ScanScreen( BackHandler(sheetState.isVisible) { coroutineScope.launch { sheetState.hide() } } + val featureToggleManager = FeatureToggleManager(context) + val directRedeemEnabled by produceState(false) { + featureToggleManager.isFeatureEnabled(Features.REDEEM_WITHOUT_TI.featureName).first().apply { + value = this + } + } if (cancelRequested && state.hasCodesToSave()) { SaveDialog( @@ -190,12 +218,20 @@ fun ScanScreen( sheetState = sheetState, sheetContent = { SheetContent( + directRedeemEnabled = directRedeemEnabled, onClickSave = { coroutineScope.launch { scanPrescriptionController.saveToDatabase() + tracker.trackSaveScannedPrescriptions() + mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) + } + }, + onClickRedeem = { + coroutineScope.launch { + scanPrescriptionController.saveToDatabase() + tracker.trackSaveScannedPrescriptions() + mainNavController.navigate(MainNavigationScreens.Redeem.path()) } - tracker.trackSaveScannedPrescriptions() - mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) } ) } @@ -244,7 +280,9 @@ fun ScanScreen( @Composable private fun SheetContent( - onClickSave: () -> Unit + onClickSave: () -> Unit, + onClickRedeem: () -> Unit, + directRedeemEnabled: Boolean ) { SpacerMedium() Text( @@ -255,29 +293,30 @@ private fun SheetContent( ) SpacerSmall() BottomSheetAction( - enabled = false, + enabled = directRedeemEnabled, icon = { Icon(Icons.Rounded.ShoppingBag, null) }, title = { Row(verticalAlignment = Alignment.CenterVertically) { Text(stringResource(R.string.cam_next_sheet_order_now_title)) SpacerSmall() - Surface( - color = AppTheme.colors.primary100, - contentColor = AppTheme.colors.primary600, - shape = RoundedCornerShape(8.dp) - ) { - Text( - stringResource(R.string.cam_next_sheet_available_soon), - Modifier.padding(horizontal = PaddingDefaults.Small, vertical = 2.dp) - ) + if (!directRedeemEnabled) { + Surface( + color = AppTheme.colors.primary100, + contentColor = AppTheme.colors.primary600, + shape = RoundedCornerShape(8.dp) + ) { + Text( + stringResource(R.string.cam_next_sheet_available_soon), + Modifier.padding(horizontal = PaddingDefaults.Small, vertical = 2.dp) + ) + } } } }, info = { Text(stringResource(R.string.cam_next_sheet_order_now_info)) }, - modifier = Modifier.fillMaxWidth() - ) { - // TODO - currently disabled; teaser for prescription orders without eGK - } + modifier = Modifier.fillMaxWidth(), + onClick = onClickRedeem + ) BottomSheetAction( icon = Icons.Rounded.SaveAlt, title = stringResource(R.string.cam_next_sheet_order_later_title), diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt index 5fba575d..2582b3db 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt @@ -221,12 +221,18 @@ fun UnknownStatusChip() = ) @Composable -fun DirectAssignmentStatusChip() = +fun DirectAssignmentStatusChip(redeemed: Boolean) { + val text = if (redeemed) { + stringResource(R.string.prescription_status_direct_assignment_closed) + } else { + stringResource(R.string.prescription_status_direct_assignment_ready) + } StatusChip( - text = stringResource(R.string.prescription_status_direct_assignment), + text = text, textColor = AppTheme.colors.neutral600, backgroundColor = AppTheme.colors.neutral200 ) +} @Composable fun DirectAssignmentChip( diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt index f43fa8fe..f08e40e3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt @@ -23,8 +23,10 @@ import TokenScreen import androidx.compose.runtime.Composable 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.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -32,6 +34,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.pkv.ui.InvoiceDetailsScreen @@ -46,16 +49,16 @@ import kotlinx.coroutines.launch object ProfileDestinations { object Profile : Route("profile") - object Token : Route("token") - object AuditEvents : Route("auditEvents") - object PairedDevices : Route("pairedDevices") - object ProfileImagePicker : Route("profileImagePicker") - object ProfileImageCropper : Route("imageCropper") - object Invoices : Route("invoices") + object Token : Route("profile_token") + object AuditEvents : Route("profile_auditEvents") + object PairedDevices : Route("profile_registeredDevices") + object ProfileImagePicker : Route("profile_editPicture") + object ProfileImageCropper : Route("profile_editPicture_imageCropper") + object Invoices : Route("chargeItem_list") object InvoiceInformation : Route( - "invoiceInformation", + "chargeItem_details", navArgument("taskId") { type = NavType.StringType } ) { fun path(taskId: String) = path("taskId" to taskId) @@ -63,7 +66,7 @@ object ProfileDestinations { object InvoiceDetails : Route( - "invoiceDetails", + "chargeItem_details_expanded", navArgument("taskId") { type = NavType.StringType } ) { fun path(taskId: String) = path("taskId" to taskId) @@ -71,7 +74,7 @@ object ProfileDestinations { object ShareInformation : Route( - "shareInformation", + "chargeItem_share", navArgument("taskId") { type = NavType.StringType } ) { fun path(taskId: String) = path("taskId" to taskId) @@ -90,6 +93,8 @@ fun EditProfileNavGraph( onRemoveProfile: (newProfileName: String?) -> Unit, mainNavController: NavController ) { + var previousNavEntry by remember { mutableStateOf("profile") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val scope = rememberCoroutineScope() val invoicesController = rememberInvoicesController(profileId = selectedProfile.id) NavHost(navController = navController, startDestination = ProfileDestinations.Profile.route) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt index cab1d215..becf4aaa 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt @@ -20,6 +20,9 @@ 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 @@ -40,15 +43,16 @@ fun RedeemNavigation( val orderState = rememberPharmacyOrderState() val navController = rememberNavController() - val navigationMode by navController.navigationModeState(RedeemNavigation.HowToRedeem.route) + val navigationMode by navController.navigationModeState(RedeemNavigation.MethodSelection.route) - TrackNavigationChanges(navController) + var previousNavEntry by remember { mutableStateOf("redeem_methodSelection") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, - startDestination = RedeemNavigation.HowToRedeem.route + startDestination = RedeemNavigation.MethodSelection.route ) { - composable(RedeemNavigation.HowToRedeem.route) { + composable(RedeemNavigation.MethodSelection.route) { NavigationAnimation(mode = navigationMode) { val prescriptions by orderState.prescriptions HowToRedeem( diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt index b9f19438..23d11c4d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt @@ -21,9 +21,9 @@ package de.gematik.ti.erp.app.redeem.ui.model import de.gematik.ti.erp.app.Route class RedeemNavigation { - object HowToRedeem : Route("redeem_how_to") - object PrescriptionSelection : Route("redeem_prescription_selection") - object LocalRedeem : Route("redeem_local") - object OnlineRedeem : Route("redeem_online") - object PharmacySearch : Route("redeem_pharmacy_search") + object MethodSelection : Route("redeem_methodSelection") + object PrescriptionSelection : Route("redeem_prescriptionChooseSubset") + object LocalRedeem : Route("redeem_matrixCode") + object OnlineRedeem : Route("redeem_prescriptionAllOrSelection") + object PharmacySearch : Route("pharmacySearch") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt index 4f28835a..8a5ac475 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt @@ -37,11 +37,10 @@ import de.gematik.ti.erp.app.utils.compose.createToastShort import kotlinx.coroutines.launch object SettingsNavigationScreens { - object Settings : Route("Settings") - - object AccessibilitySettings : Route("AccessibilitySettings") - object ProductImprovementSettings : Route("ProductImprovementSettings") - object DeviceSecuritySettings : Route("DeviceSecuritySettings") + object Settings : Route("settings") + object AccessibilitySettings : Route("settings_accessibility") + object ProductImprovementSettings : Route("settings_productImprovements") + object DeviceSecuritySettings : Route("settings_authenticationMethods") } @Suppress("LongMethod") diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt index 15d46b95..24e19a6b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt @@ -88,6 +88,7 @@ import de.gematik.ti.erp.app.BuildConfig import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardwall.usecase.deviceHasNFC import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens @@ -117,7 +118,8 @@ fun SettingsScreen( settingsController: SettingsController ) { val settingsNavController = rememberNavController() - + var previousNavEntry by remember { mutableStateOf("settings") } + TrackNavigationChanges(settingsNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by settingsNavController.navigationModeState(SettingsNavigationScreens.Settings.route) SettingsNavGraph( diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt index a5761d0d..cfb28088 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt @@ -58,7 +58,7 @@ class SettingsUseCase( } // end::ShowInsecureDevicePrompt[] - val showOnboarding = settingsRepository.general.map { it.onboardingShownIn == null } + val showOnboarding = settingsRepository.general.map { it.onboardingShownIn == null } // TODO Move to Mainscreen val showWelcomeDrawer = settingsRepository.general.map { !it.welcomeDrawerShown } val showMainScreenTooltip: Flow = settingsRepository.general.map { !it.mainScreenTooltipsShown } diff --git a/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt b/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt index bf05057e..be1e0308 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt @@ -49,11 +49,16 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +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.R import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.settings.ui.buildFeedbackBodyWithDeviceInfo import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.AppTheme @@ -72,10 +77,10 @@ import de.gematik.ti.erp.app.utils.compose.annotatedLinkString import de.gematik.ti.erp.app.utils.compose.annotatedStringResource object TroubleShootingNavigation { - object TroubleshootingPageA : Route("TroubleshootingPageA") - object TroubleshootingPageB : Route("TroubleshootingPageB") - object TroubleshootingPageC : Route("TroubleshootingPageC") - object TroubleshootingNoSuccessPage : Route("TroubleshootingNoSuccessPage") + object TroubleshootingPageA : Route("troubleShooting") + object TroubleshootingPageB : Route("troubleShooting_readCardHelp1") + object TroubleshootingPageC : Route("troubleShooting_readCardHelp2") + object TroubleshootingNoSuccessPage : Route("troubleShooting_readCardHelp3") } @Composable @@ -84,7 +89,8 @@ fun TroubleShootingScreen( onCancel: () -> Unit ) { val navController = rememberNavController() - + var previousNavEntry by remember { mutableStateOf("troubleShooting") } + TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, startDestination = TroubleShootingNavigation.TroubleshootingPageA.path() diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt index 9fe5035c..dc7993d6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt @@ -376,7 +376,7 @@ fun HintTextActionButton( @Composable fun HintTextLearnMoreButton( modifier: Modifier = Modifier, - uri: String = "https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten", + uri: String = stringResource(R.string.auth_link_to_gematik_q_and_a), align: Alignment.Horizontal = Alignment.Start ) { val uriHandler = LocalUriHandler.current diff --git a/android/src/main/res/raw/analytics_identifier.json b/android/src/main/res/raw/analytics_identifier.json new file mode 100644 index 00000000..9518c43c --- /dev/null +++ b/android/src/main/res/raw/analytics_identifier.json @@ -0,0 +1,487 @@ +[ + { + "main": { + "name": "main" + } + }, + { + "main_createProfile": { + "name": "main:createProfile" + } + }, + { + "main_editProfilePicture": { + "name": "main:editProfilePicture" + } + }, + { + "main_editName": { + "name": "main:editName" + } + }, + { + "main_scanner": { + "name": "main:scanner" + } + }, + { + "main_deviceSecurity": { + "name": "main:deviceSecurity" + } + }, + { + "main_integrityWarning": { + "name": "main:integrityWarning" + } + }, + { + "main_prescriptionArchive": { + "name": "main:prescriptionArchive" + } + }, + { + "main_welcomeDrawer": { + "name": "main:welcomeDrawer" + } + }, + { + "prescriptionDetail": { + "name": "prescriptionDetail" + } + }, + { + "prescriptionDetail_medication": { + "name": "prescriptionDetail:medication" + } + }, + { + "prescriptionDetail_patient": { + "name": "prescriptionDetail:patient" + } + }, + { + "prescriptionDetail_practitioner": { + "name": "prescriptionDetail:practitioner" + } + }, + { + "prescriptionDetail_organization": { + "name": "prescriptionDetail:organization" + } + }, + { + "prescriptionDetail_accidentInfo": { + "name": "prescriptionDetail:accidentInfo" + } + }, + { + "prescriptionDetail_technicalInfo": { + "name": "prescriptionDetail:technicalInfo" + } + }, + { + "prescriptionDetail_sharePrescription": { + "name": "prescriptionDetail:sharePrescription" + } + }, + { + "prescriptionDetail_directAssignmentInfo": { + "name": "prescriptionDetail:directAssignmentInfo" + } + }, + { + "prescriptionDetail_substitutionInfo": { + "name": "prescriptionDetail:substitutionInfo" + } + }, + { + "prescriptionDetail_errorInfo": { + "name": "prescriptionDetail:errorInfo" + } + }, + { + "prescriptionDetail_prescriptionValidityInfo": { + "name": "prescriptionDetail:prescriptionValidityInfo" + } + }, + { + "prescriptionDetail_scannedPrescriptionInfo": { + "name": "prescriptionDetail:scannedPrescriptionInfo" + } + }, + { + "prescriptionDetail_coPaymentInfo": { + "name": "prescriptionDetail:coPaymentInfo" + } + }, + { + "prescriptionDetail_emergencyServiceFeeInfo": { + "name": "prescriptionDetail:emergencyServiceFeeInfo" + } + }, + { + "prescriptionDetail_medicationOverview": { + "name": "prescriptionDetail:medicationOverview" + } + }, + { + "prescriptionDetail_medication_ingredients": { + "name": "prescriptionDetail:medication_ingredients" + } + }, + { + "redeem_methodSelection": { + "name": "redeem:methodSelection" + } + }, + { + "redeem_prescriptionAllOrSelection": { + "name": "redeem:prescriptionAllOrSelection" + } + }, + { + "redeem_prescriptionChooseSubset": { + "name": "redeem:prescriptionChooseSubset" + } + }, + { + "redeem_matrixCode": { + "name": "redeem:matrixCode" + } + }, + { + "redeem_viaAVS": { + "name": "redeem:viaAVS" + } + }, + { + "redeem_viaTI": { + "name": "redeem:viaTI" + } + }, + { + "redeem_success": { + "name": "redeem:success" + } + }, + { + "redeem_editContactInformation": { + "name": "redeem:editContactInformation" + } + }, + { + "pharmacySearch": { + "name": "pharmacySearch" + } + }, + { + "pharmacySearch_detail": { + "name": "pharmacySearch:detail" + } + }, + { + "pharmacySearch_filter": { + "name": "pharmacySearch:filter" + } + }, + { + "pharmacySearch_map": { + "name": "pharmacySearch:map" + } + }, + { + "pharmacySearch_selectedPharmacy": { + "name": "pharmacySearch:selectedPharmacy" + } + }, + { + "cardWall": { + "name": "cardWall" + } + }, + { + "cardWall_introduction": { + "name": "cardWall:welcome" + } + }, + { + "cardWall_notCapable": { + "name": "cardWall:notCapable" + } + }, + { + "cardWall_CAN": { + "name": "cardWall:CAN" + } + }, + { + "cardWall_PIN": { + "name": "cardWall:PIN" + } + }, + { + "cardWall_scanCAN": { + "name": "cardWall:scanCAN" + } + }, + { + "cardWall_saveLogin": { + "name": "cardWall:saveCredentials:initial" + } + }, + { + "cardWall_saveLoginSecurityInfo": { + "name": "cardWall:saveCredentials:information" + } + }, + { + "cardWall_readCard": { + "name": "cardWall:connect" + } + }, + { + "cardWall_extAuth": { + "name": "cardWall:extAuth" + } + }, + { + "cardWall_extAuthConfirm": { + "name": "cardWall:extAuthConfirm" + } + }, + { + "troubleShooting": { + "name": "troubleShooting" + } + }, + { + "troubleShooting_readCardHelp1": { + "name": "troubleShooting:readCardHelp1" + } + }, + { + "troubleShooting_readCardHelp2": { + "name": "troubleShooting:readCardHelp2" + } + }, + { + "troubleShooting_readCardHelp3": { + "name": "troubleShooting:readCardHelp3" + } + }, + { + "contactInsuranceCompany": { + "name": "contactInsuranceCompany" + } + }, + { + "contactInsuranceCompany_selectKK": { + "name": "contactInsuranceCompany:selectKK" + } + }, + { + "contactInsuranceCompany_selectReason": { + "name": "contactInsuranceCompany:selectReason" + } + }, + { + "contactInsuranceCompany_selectMethod": { + "name": "contactInsuranceCompany:selectMethod" + } + }, + { + "orders": { + "name": "orders" + } + }, + { + "orders_detail": { + "name": "orders:detail" + } + }, + { + "orders_pickupCode": { + "name": "orders:pickupCode" + } + }, + { + "alert": { + "name": "General Alert Dialog" + } + }, + { + "errorAlert": { + "name": "Error Alert" + } + }, + { + "healthCardPassword_forgotPin": { + "name": "healthCardPassword:forgotPin" + } + }, + { + "healthCardPassword_setCustomPin": { + "name": "healthCardPassword:setCustomPin" + } + }, + { + "healthCardPassword_unlockCard": { + "name": "healthCardPassword:unlockCard" + } + }, + { + "healthCardPassword_introduction": { + "name": "healthCardPassword:introduction" + } + }, + { + "healthCardPassword_can": { + "name": "healthCardPassword:can" + } + }, + { + "healthCardPassword_puk": { + "name": "healthCardPassword:puk" + } + }, + { + "healthCardPassword_oldPin": { + "name": "healthCardPassword:oldPin" + } + }, + { + "healthCardPassword_pin": { + "name": "healthCardPassword:pin" + } + }, + { + "healthCardPassword_readCard": { + "name": "healthCardPassword:readCard" + } + }, + { + "healthCardPassword_scanner": { + "name": "healthCardPassword:scanner" + } + }, + { + "settings": { + "name": "settings" + } + }, + { + "settings_accessibility": { + "name": "settings:accessibility" + } + }, + { + "settings_authenticationMethods": { + "name": "settings:authenticationMethods" + } + }, + { + "settings_authenticationMethods_setAppPassword": { + "name": "settings:authenticationMethods:setAppPassword" + } + }, + { + "settings_productImprovements": { + "name": "settings:productImprovements" + } + }, + { + "settings_productImprovements_complyTracking": { + "name": "settings:productImprovements:complyTracking" + } + }, + { + "settings_legalNotice": { + "name": "settings:legalNotice" + } + }, + { + "settings_dataProtection": { + "name": "settings:dataProtection" + } + }, + { + "settings_openSourceLicence": { + "name": "settings:openSourceLicence" + } + }, + { + "settings_additionalLicence": { + "name": "settings:additionalLicence" + } + }, + { + "settings_termsOfUse": { + "name": "settings:termsOfUse" + } + }, + { + "profile": { + "name": "profile" + } + }, + { + "profile_editPicture": { + "name": "profile:editPicture" + } + }, + { + "profile_editPicture_imageCropper": { + "name": "profile:editPicture:imageCropper" + } + }, + { + "settings_newProfile": { + "name": "settings:newProfile" + } + }, + { + "profile_token": { + "name": "profile:token" + } + }, + { + "profile_registeredDevices": { + "name": "profile:registeredDevices" + } + }, + { + "profile_auditEvents": { + "name": "profile:auditEvents" + } + }, + { + "chargeItem_list": { + "name": "chargeItem:list" + } + }, + { + "chargeItem_details": { + "name": "chargeItem:details" + } + }, + { + "chargeItem_details_expanded": { + "name": "chargeItem:details:expanded" + } + }, + { + "chargeItem_share": { + "name": "chargeItem:share" + } + }, + { + "mlKit": { + "name": "mlKit" + } + }, + { + "mlKit_information": { + "name": "mlKit:information" + } + } +] diff --git a/android/src/main/res/raw/nfc_positions.json b/android/src/main/res/raw/nfc_positions.json index d1817729..1cc87b42 100644 --- a/android/src/main/res/raw/nfc_positions.json +++ b/android/src/main/res/raw/nfc_positions.json @@ -1635,5 +1635,79 @@ "x1": 0.6, "y1": 0.35294117647058826 } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A34 5G", + "modelNames": [ + "SM-A3460", + "SM-A346B", + "SM-A346E", + "SM-A346M", + "SM-A346N" + ], + "nfcPos": { + "x0": 0.04054054054054057, + "y0": 0.016835016835016835, + "x1": 0.7297297297297297, + "y1": 0.27946127946127947 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23 Ultra", + "modelNames": [ + "SC-52D", + "SM-S9180", + "SM-S918B", + "SM-S918N", + "SM-S918U", + "SM-S918U1", + "SM-S918W" + ], + "nfcPos": { + "x0": 0.05405405405405406, + "y0": 0.3468013468013468, + "x1": 0.9324324324324325, + "y1": 0.8316498316498316 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23+", + "modelNames": [ + "SM-S9160", + "SM-S916B", + "SM-S916N", + "SM-S916U", + "SM-S916U1", + "SM-S916W" + ], + "nfcPos": { + "x0": 0.03355704697986572, + "y0": 0.33557046979865773, + "x1": 0.9463087248322147, + "y1": 0.7550335570469798 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23", + "modelNames": [ + "SC-51D", + "SM-S9110", + "SM-S911B", + "SM-S911C", + "SM-S911N", + "SM-S911U", + "SM-S911U1", + "SM-S911W" + ], + "nfcPos": { + "x0": 0.04054054054054057, + "y0": 0.34459459459459457, + "x1": 0.9459459459459459, + "y1": 0.7533783783783784 + } } ] \ No newline at end of file diff --git a/android/src/main/res/values-ar/strings.xml b/android/src/main/res/values-ar/strings.xml index c1f9f8d6..3ca2672c 100644 --- a/android/src/main/res/values-ar/strings.xml +++ b/android/src/main/res/values-ar/strings.xml @@ -35,7 +35,6 @@ إدخال رقم التعريف الشخصي جرب مرة أخرى فشل الاتصال بالخادم. - تم إدخال رقم تعريف شخصي خاطيء. لديك %s محاولة أخرى قبل وقف البطاقة. @@ -44,7 +43,6 @@ لديك %s محاولات أخرى قبل وقف البطاقة. - تم إدخال CAN خاطيء تجد رقم تسجيل الدخول أعلى يمينًا في بطاقتك الصحية. إلغاء البحث عن بطاقة... @@ -237,8 +235,8 @@ فشل الاتصال بالسيرفر. كود الحالة %s. فشل الاتصال بالسيرفر. خطأ VAU تحذير - لا يحظى هذا الجهاز بالثقة الكاملة - لأسباب تتعلق بالأمن، لا يُنصح باستخدام هذا التطبيق بالأجهزة التي تعمل بنظام التجذير. + قد يكون جهازك قد قلل من مستوى الأمان + يمكن أن يحدث هذا ، على سبيل المثال ، عن طريق الأجهزة التي تم التلاعب بها أو وضع المطور المنشط. لأسباب أمنية ، لا نوصي باستخدام التطبيق على أجهزة مكسورة الحماية. أنا على دراية بالمخاطر ومع ذلك أرغب في المواصلة. لماذا تعتبر الأجهزة التي تعمل بنظام التجذير أحد المخاطر الأمنية المحتملة؟ معرفة المزيد @@ -453,7 +451,6 @@ سجلت رقم التعريف الشخصي؟ يرجى تدوين رقم التعريف الشخصي الخاص بك والاحتفاظ به في مكان آمن. إلغاء - تم إدخال رمز PUK خاطيء. موافق لا يمكن إلغاء الحظر لقد وصلك باستخدام مفتاح فك القفل الشخصي إلى العدد الأقصى لعمليات إلغاء الحظر بالبطاقات أو أدخلته بشكل خاطيء مرات متكررة. يُرجى التوجه إلى شركة التأمين الخاصة بك. @@ -587,7 +584,6 @@ حاليا مفتوحة وقريبة مني مصنف بواسطة … ابدأ البحث - تم استبدالها من أجلك الاحالة المباشرة الصيدليات رقم الهاتف (اختيارى) @@ -667,8 +663,8 @@ استلام ما هو التعيين المباشر؟ في حالة الإحالات المباشرة ، يتم استرداد وصفة طبية من عيادة أو مستشفى مباشرة في الصيدلية. لا يتعين على الأشخاص المؤمن عليهم اتخاذ أي إجراء ولا يمكنهم التدخل في عملية الاسترداد. \n\n يتم سرد الإحالات المباشرة في تطبيق الوصفات الطبية الإلكترونية لجعل علاجك أكثر شفافية بالنسبة لك. - لا توجد رسوم خدمة الطوارئ - على عجل هو ترتيب اليوم هنا. يمكن أيضًا ملء هذه الوصفة الطبية ليلًا في الصيدلية دون دفع رسوم خدمة الطوارئ الإضافية. + رسوم خدمة الطوارئ + في بعض الأحيان هناك حاجة للتسرع. يمكن استبدال بعض الوصفات الطبية دون دفع رسوم خدمة الطوارئ الإضافية ، مثل الليل أو في أيام العطلات. الأدوية الخاضعة للدفع المشترك معفى من المشاركة يجب على أولئك الذين لديهم تأمين صحي قانوني دفع مبلغ مشترك يصل إلى عشرة يورو للأدوية الموصوفة. \n\n يعتمد مبلغ الدفع المشترك على سعر الدواء الخاص بك. عليك أن تدفع ثمن الأدوية التي تقل تكلفتها عن 5 يورو بنفسك.\n بالنسبة للأدوية الأكثر تكلفة ، عليك أن تدفع عشرة بالمائة من السعر ، ولكن على الأقل 5 يورو و 10 يورو كحد أقصى. \n\n يُعفى الأطفال والشباب الذين تقل أعمارهم عن 18 عامًا بشكل عام من المشاركة في الدفع. \n\n إذا تجاوزت تكاليف الأدوية السنوية الخاصة بك الحد المالي الخاص بك ، فيمكن إعفائك من الدفع المشترك. تحدث إلى شركة التأمين الصحي الخاصة بك حول هذا الموضوع. @@ -838,4 +834,58 @@ تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية إغلاق + تم إدخال رقم التعريف الشخصي بشكل غير صحيح. + تم إدخال رقم الوصول بشكل غير صحيح + تم إدخال PUK بشكل غير صحيح. + إيصالات المصروفات + إظهار إيصالات المصروفات + إيصالات المصروفات + لتلقي إيصالات النفقات ، يجب أن تكون متصلاً بالخادم. + التوصيل + لا إيصالات حساب + تعطيل + إلغاء + تعطيل وظيفة + سيؤدي هذا إلى حذف جميع الإيصالات من هذا الجهاز ومن الخادم. + استلام إيصالات المصروفات + يتم أيضًا حفظ إيصالات التكلفة الخاصة بك على خادم الوصفات. + يستلم + الإجمالي: %s %s + الاختيار + ينقسم + حذف + حذف + يُقدِّم + %s € + السعر الكلي + نصيحة: أرسل إيصالات النفقات عبر تطبيق التأمين + قم بإرسال إيصالات التكلفة بسهولة عبر تطبيق شركة التأمين الخاصة بك. في الخطوة التالية ، حدد هذا التطبيق واضغط على مشاركة. + عياده + الصيدلية + تاريخ + عرض المزيد + معرف الدواء + أصدرت ل + KVNR: %s + تاريخ الميلاد: %s + موافق + كيف ترسل الإيصالات؟ + التحويل مباشرة إلى التطبيق الخاص بشركة التأمين / مكتب المساعدة. للقيام بذلك ، حدد التطبيق في الصفحة التالية. + أو + احفظ الملف واستورده لاحقًا إلى بوابة التأمين / المساعدة. + مقال: %s + الرقم: %s + ضريبة القيمة المضافة: %s %% + السعر الإجمالي باليورو: %s + رسوم اضافية + رسوم خدمة الطوارئ + رسوم BTM + رسوم الوصفة T. + تكاليف الشراء + خدمة المراسلة + الإجمالي باليورو: %s + الجبايه + هل ترغب حقًا في الحذف؟ + سيتم حذف الملف من جهازك ومن الخادم. + حذف diff --git a/android/src/main/res/values-en/strings.xml b/android/src/main/res/values-en/strings.xml index f63afd44..acf9c6c9 100644 --- a/android/src/main/res/values-en/strings.xml +++ b/android/src/main/res/values-en/strings.xml @@ -31,12 +31,10 @@ Enter PIN Try again Failed to connect to the server. - Incorrect PIN entered. You have %s attempt remaining before your card is locked. You have %s attempts remaining before your card is locked. - Incorrect CAN entered You will find the access number in the top right-hand corner of your medical card. Cancel Searching for card... @@ -217,8 +215,8 @@ Communication with the server failed: status code %s. Communication with the server failed: VAU error Warning - This device may not be fully trustworthy - This app should not be used on rooted devices for security reasons. + Your device may have reduced security + This can be caused, for example, by manipulated devices or an activated developer mode. For security reasons, we do not recommend using the app on jailbroken devices. I acknowledge the increased risk and would like to continue anyway. Why are devices with root access a potential security risk? Find out more @@ -425,7 +423,6 @@ PIN remembered? Please make a note of your PIN and keep it in a safe place. Cancel - Incorrect PUK entered. OK Cannot unlock You\'ve used this PUK to unlock your card the maximum number of times or have repeatedly entered it incorrectly. Please contact your health insurance company. @@ -555,7 +552,6 @@ Currently open and near me Filter by … start search - Was redeemed for you direct assignment pharmacies phone number (optional) @@ -631,8 +627,8 @@ Receive What is a direct assignment? In the case of direct referrals, a prescription from a practice or hospital is redeemed 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. - No emergency service fee - Hurry is the order of the day here. This prescription can also be filled at night in a pharmacy without the additional payment of an emergency service fee. + 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. Drugs subject to co-payment Exempted from co-payment Those with statutory health insurance must pay a co-payment of up to ten euros for prescription drugs. \n\n The amount of the co-payment depends on the price of your medication. You have to pay for medicines 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 co-payment. \n\n If your annual costs for medication exceed your financial limit, you can be exempted from the co-payment. Talk to your health insurer about this. @@ -798,4 +794,58 @@ Your insurance company offers the following contact options Your insurance company offers the following contact options Close + PIN entered incorrectly. + Access number entered incorrectly + PUK entered incorrectly. + expense receipts + Show expense receipts + expense receipts + To receive expense receipts, you must be connected to the server. + Connect + No expense receipts + Disable + Cancel + disable function + This will delete all receipts from this device and from the server. + Receive expense receipts + Your cost receipts are also saved on the recipe server. + Receive + Total: %s %s + Select + Split + Delete + Delete + Submit + %s € + total price + Tip: Submit expense 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 + Pharmacy + Date + Show more + Drug ID + Issued for + KVNR: %s + Date of birth: %s + OK + How do you submit receipts? + Transfer directly to the app of your insurance company/aid office. To do this, select the app on the next page. + or + Save the file and later import it into the insurance/aid portal. + Article: %s + Number: %s + VAT: %s %% + Gross price in EUR: %s + Additional Fees + Emergency service fee + BTM fee + T prescription fee + procurement costs + courier service + Total in EUR: %s + levy + Really delete? + The file will be deleted from your device and from the server. + Delete diff --git a/android/src/main/res/values-pl/strings.xml b/android/src/main/res/values-pl/strings.xml index 1ff90cb0..9c81d5a9 100644 --- a/android/src/main/res/values-pl/strings.xml +++ b/android/src/main/res/values-pl/strings.xml @@ -33,14 +33,12 @@ Wprowadź PIN Spróbuj ponownie Nie udało się utworzyć połączenia z serwerem. - Wprowadzono błędny PIN. Masz jeszcze %s próbę, zanim Twoja karta zostanie zablokowana. Masz jeszcze %s próby, zanim Twoja karta zostanie zablokowana. Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. - Wprowadzono błędny CAN Numer dostępu znajduje się na górze z prawej strony Twojej karty zdrowia. Anuluj Szukaj karty... @@ -227,8 +225,8 @@ Nie udało się nawiązać połączenia z serwerem: kod statusu %s. Nie udało się nawiązać połączenia z serwerem: błąd VAU Ostrzeżenie - Takiemu urządzeniu nie należy w pełni ufać - Ze względów bezpieczeństwa nie powinno się używać tej aplikacji na urządzeniach zrootowanych. + Twoje urządzenie mogło mieć obniżone zabezpieczenia + Może to być spowodowane na przykład manipulowaniem urządzeniami lub aktywowanym trybem programisty. Ze względów bezpieczeństwa nie zalecamy korzystania z aplikacji na urządzeniach z jailbreakiem. Akceptuję zwiększone ryzyko i mimo to chcę kontynuować. Dlaczego urządzenia z dostępem root stwarzają potencjalne zagrożenie dla bezpieczeństwa? Dowiedz się więcej @@ -439,7 +437,6 @@ Pamiętasz kod PIN? Zanotuj swój kod PIN i przechowuj go w bezpiecznym miejscu. Anuluj - Wprowadzono błędny PUK. OK Odblokowanie jest niemożliwe Za pomocą tego kodu PUK została wykorzystana maksymalna liczba odblokowań karty lub kod był wielokrotnie błędnie wprowadzany. Skontaktuj się ze swoim ubezpieczycielem. @@ -571,7 +568,6 @@ Obecnie otwarte i blisko mnie Filtruj według … Zacznij szukać - Został odkupiony dla ciebie bezpośrednie przypisanie apteki numer telefonu (opcjonalnie) @@ -649,8 +645,8 @@ Odbierać Czym jest przydział bezpośredni? W przypadku skierowań bezpośrednich, receptę z przychodni lub szpitala realizuje się bezpośrednio w aptece. Ubezpieczeni nie muszą podejmować żadnych działań i nie mogą ingerować w proces wykupu. \n\n Bezpośrednie skierowania są wymienione w aplikacji e-recept, aby Twoje leczenie było dla Ciebie bardziej przejrzyste. - Brak opłaty za usługi ratunkowe - Pośpiech jest tutaj na porządku dziennym. Receptę tę można również zrealizować w nocy w aptece bez dodatkowej opłaty za pomoc w nagłych wypadkach. + 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. Leki objęte współpłatnością Zwolnione ze współpłacenia Osoby posiadające ustawowe ubezpieczenie zdrowotne muszą zapłacić do dziesięciu euro za leki na receptę. \n\n Wysokość dopłaty zależy od ceny leku. Sam musisz zapłacić za leki, które kosztują mniej niż 5 euro.\n W przypadku droższych leków trzeba zapłacić dziesięć procent ceny, ale co najmniej 5 euro, a maksymalnie 10 euro. \n\n Dzieci i młodzież poniżej 18 roku życia są z reguły zwolnione ze współpłacenia. \n\n Jeśli Twoje roczne koszty leków przekraczają Twój limit finansowy, możesz zostać zwolniony ze współpłacenia. Porozmawiaj o tym ze swoim ubezpieczycielem zdrowotnym. @@ -818,4 +814,58 @@ Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu Zamknij + Kod PIN wprowadzony nieprawidłowo. + Numer dostępu wpisany nieprawidłowo + Kod PUK został wprowadzony nieprawidłowo. + rachunki za wydatki + Pokaż rachunki za wydatki + rachunki za wydatki + Aby otrzymywać rachunki za wydatki, musisz mieć połączenie z serwerem. + Nawiąż połączenie + Brak rachunków za wydatki + Dezaktywuj + Anuluj + wyłączyć funkcję + Spowoduje to usunięcie wszystkich paragonów z tego urządzenia iz serwera. + Otrzymuj rachunki za wydatki + Rachunki poniesionych kosztów są również zapisywane na serwerze receptur. + Odbierać + Razem: %s %s + Wybierz + Podział + Usuń + Usuń + Składać + %s € + cena całkowita + Wskazówka: prześlij rachunki za wydatki za pośrednictwem aplikacji ubezpieczeniowej + Łatwe przesyłanie rachunków kosztów za pośrednictwem aplikacji firmy ubezpieczeniowej. W następnym kroku wybierz tę aplikację i naciśnij Udostępnij. + Ćwiczyć + Apteka + Data + Wyświetl więcej + Identyfikator leku + Wydana dla + KVNR: %s + Data urodzenia: %s + OK + Jak przesyłać rachunki? + Przenieś bezpośrednio do aplikacji swojej firmy ubezpieczeniowej / biura pomocy. Aby to zrobić, wybierz aplikację na następnej stronie. + Lub + Zapisz plik, a następnie zaimportuj go do portalu ubezpieczenia/pomocy. + Artykuł: %s + Numer: %s + VAT: %s %% + Cena brutto w EUR: %s + Dodatkowe opłaty + Opłata za obsługę w nagłych wypadkach + Opłata BTM + Opłata za receptę T + koszty zaopatrzenia + Usługa kurierska + Suma w EUR: %s + nałożyć + Naprawdę usunąć? + Plik zostanie usunięty z Twojego urządzenia i serwera. + Usuń diff --git a/android/src/main/res/values-ru/strings.xml b/android/src/main/res/values-ru/strings.xml index 46ed199d..2568b007 100644 --- a/android/src/main/res/values-ru/strings.xml +++ b/android/src/main/res/values-ru/strings.xml @@ -33,14 +33,12 @@ Ввести PIN-код Попробовать снова Не удалось подключиться к серверу. - Введен неправильный PIN-код. У вас осталась еще %s попытка, прежде чем ваша карточка будет заблокирована. У вас осталось еще %s попытки, прежде чем ваша карточка будет заблокирована. У вас осталась еще %s попыток, прежде чем ваша карточка будет заблокирована. У вас осталось еще %s попыток, прежде чем ваша карточка будет заблокирована. - Введен неправильный CAN Номер доступа указан в верхнем правом углу медицинской карточки. Отмена Поиск карточки... @@ -227,8 +225,8 @@ Не удалось установить соединение с сервером: код состояния %s. Не удалось установить соединение с сервером: ошибка VAU Предупреждение - Возможно, этому устройству нельзя полностью доверять - В целях безопасности это приложение не следует использовать на устройствах с корневым доступом. + Ваше устройство может иметь пониженную безопасность + Это может быть вызвано, например, управляемыми устройствами или активированным режимом разработчика. Из соображений безопасности мы не рекомендуем использовать приложение на взломанных устройствах. Я понимаю повышенный уровень риска и, несмотря на это, хочу продолжить. Почему устройства с корневым доступом потенциально небезопасны? Узнать больше @@ -439,7 +437,6 @@ Запомнили PIN-код? Запишите свой PIN-код и сохраните записку в надежном месте. Отмена - Введен неправильный PUK-код. OK Деблокировка невозможна Вы достигли максимального количества операций деблокировки с помощью этого PUK-кода либо повторно ввели неправильный код. Обратитесь в свою страховую организацию. @@ -571,7 +568,6 @@ В настоящее время открыто и рядом со мной Сортировать по … начать поиск - Был искуплен за тебя прямое назначение аптеки Телефонный номер (не обязательно) @@ -649,8 +645,8 @@ Получать Что такое прямое назначение? В случае прямых направлений рецепт из практики или больницы выкупается непосредственно в аптеке. Застрахованные не обязаны предпринимать никаких действий и не могут вмешиваться в процесс выкупа. \n\n Прямые направления перечислены в приложении электронных рецептов, чтобы сделать ваше лечение более прозрачным для вас. - Нет платы за экстренную помощь - Спешите порядок дня здесь. Этот рецепт также можно получить ночью в аптеке без дополнительной оплаты сбора за экстренную помощь. + Плата за аварийное обслуживание + Иногда возникает необходимость в спешке. Некоторые рецепты могут быть заполнены без дополнительной оплаты за услуги неотложной помощи, например, в ночное время или в праздничные дни. Препараты, подлежащие доплате Освобожден от доплаты Те, у кого есть государственная медицинская страховка, должны внести доплату в размере до десяти евро за лекарства, отпускаемые по рецепту. \n\n Размер доплаты зависит от стоимости вашего лекарства. Вы должны платить за лекарства стоимостью менее 5 евро самостоятельно.\n За более дорогие лекарства вы должны заплатить десять процентов от цены, но не менее 5 евро и не более 10 евро. \n\n Дети и молодые люди в возрасте до 18 лет, как правило, освобождаются от доплаты. \n\n Если ваши ежегодные расходы на лекарства превышают ваш финансовый лимит, вы можете быть освобождены от доплаты. Поговорите об этом со своей страховой компанией. @@ -818,4 +814,58 @@ Ваша страховая компания предлагает следующие варианты контактов Ваша страховая компания предлагает следующие варианты контактов Закрыть + PIN-код введен неправильно. + Номер доступа введен неправильно + PUK введен неверно. + квитанции о расходах + Показать квитанции о расходах + квитанции о расходах + Для получения квитанций о расходах необходимо подключение к серверу. + Установить соединение + Нет квитанций о расходах + Деактивировать + Отмена + отключить функцию + Это приведет к удалению всех квитанций с этого устройства и с сервера. + Получать квитанции о расходах + Квитанции о расходах также сохраняются на сервере рецептов. + Получать + Всего: %s %s + Выберите + Расколоть + Удалить + Удалить + Представлять на рассмотрение + %s € + Итоговая цена + Совет: отправляйте квитанции о расходах через страховое приложение. + Легко отправляйте квитанции о расходах через приложение вашей страховой компании. На следующем шаге выберите это приложение и нажмите «Поделиться». + Упражняться + Аптека + Дата + Показать больше + Идентификатор препарата + Выпущено для + КВНР: %s + Дата рождения: %s + OK + Как вы отправляете квитанции? + Передача непосредственно в приложение вашей страховой компании / бюро помощи. Для этого выберите приложение на следующей странице. + или + Сохраните файл, а затем импортируйте его на портал страхования/помощи. + Статья: %s + Номер: %s + НДС: %s %% + Цена брутто в евро: %s + Дополнительная плата + Плата за аварийное обслуживание + Комиссия BTM + Плата за рецепт Т + закупочные расходы + Курьерская доставка + Всего в евро: %s + взимать + Реально удалить? + Файл будет удален с вашего устройства и с сервера. + Удалить diff --git a/android/src/main/res/values-tr/strings.xml b/android/src/main/res/values-tr/strings.xml index a9c7c8e9..0a16dcf6 100644 --- a/android/src/main/res/values-tr/strings.xml +++ b/android/src/main/res/values-tr/strings.xml @@ -31,12 +31,10 @@ PIN girin Tekrar dene Sunucu bağlantısı başarısız oldu. - Yanlış PIN girildi. Kartınız bloke olmadan %s denemeniz kaldı. Kartınız bloke olmadan %s denemeniz kaldı. - Yanlış CAN girildi Erişim numarasını sağlık kartınızın sağ üst köşesinde bulacaksınız. İptal et Kartı ara... @@ -217,8 +215,8 @@ Sunucu ile iletişim başarısız oldu: %s durum kodu. Sunucu ile iletişim başarısız: VAU hatası Uyarı - Bu cihaza tam güvenilmemeli - Bu uygulama güvenlik nedenlerden dolayı kök erişimi bulunan cihazlarda kullanılmamalıdır. + Cihazınızın güvenliği azaltılmış olabilir + Buna, örneğin manipüle edilmiş cihazlar veya etkinleştirilmiş bir geliştirici modu neden olabilir. Güvenlik nedeniyle, uygulamanın jailbreak\'li cihazlarda kullanılmasını önermiyoruz. Yüksek riski kabul ediyor ve yine de devam etmek istiyorum. Kök erişimi bulunan cihazlar neden potansiyel bir güvenlik riski taşıyor? Daha fazla bilgi @@ -425,7 +423,6 @@ PIN\'i hafızanıza kaydettiniz mi? Lütfen PIN\'inizi not alın ve güvenli bir yerde saklayınız. İptal et - Yanlış PUK girildi. Tamam Blokenin kaldırılması mümkün değil Bu PUK ile maksimum kart bloke kaldırma sayısına ulaştınız veya tekrar yanlış girdiniz. Lütfen sigortanız ile iletişime geçin. @@ -555,7 +552,6 @@ Şu anda açık ve yakınımda Tarafından filtre … Aramaya başla - Senin için kurtarıldı doğrudan atama eczaneler telefon numarası (isteğe bağlı) @@ -631,8 +627,8 @@ Almak Doğrudan atama nedir? Doğrudan sevk durumunda, bir muayenehaneden veya hastaneden alınan reçete doğrudan eczanede kullanılır. Sigortalılar herhangi bir işlem yapmak zorunda değildir ve itfa 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 yok - Acele burada günün sırasıdır. Bu reçete ayrıca, ek bir acil servis ücreti ödemeden eczanede geceleri de doldurulabilir. + Acil servis ücreti + Bazen acele etmek gerekebilir. Bazı reçeteler, örneğin geceleri veya resmi tatil günlerinde, acil servis ücreti ödenmeden doldurulabilir. Katkı payına tabi ilaçlar Ortak ödemeden muaf Yasal sağlık sigortası olanlar, reçeteli ilaçlar için on Euro\'ya kadar katkı payı ödemek zorundadır. \n\n Katkı payı miktarı, ilacınızın fiyatına bağlıdır. Maliyeti 5 €\'dan az olan ilaçlar için kendiniz ödeme yapmanız gerekir.\n Daha pahalı ilaçlar için fiyatın yüzde onunu, ancak en az 5 € ve maksimum 10 € ödemeniz gerekir. \n\n 18 yaşın altındaki çocuklar ve gençler genellikle katkı payından muaftır. \n\n Yıllık ilaç masraflarınız mali limitinizi aşarsa, katkı payından muaf olabilirsiniz. Bu konuda sağlık sigortanız ile konuşun. @@ -798,4 +794,58 @@ Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar Kapat + PIN yanlış girildi. + Erişim numarası yanlış girildi + PUK yanlış girildi. + gider makbuzları + Gider makbuzlarını göster + gider makbuzları + Gider makbuzlarını almak için sunucuya bağlı olmanız gerekir. + Bağlan + Masraf makbuzu yok + Devre dışı bırakmak + İptal et + işlevi devre dışı bırak + Bu, bu cihazdaki ve sunucudaki tüm makbuzları siler. + Gider makbuzlarını alma + Masraf makbuzlarınız da reçete sunucusuna kaydedilir. + Almak + Toplam: %s %s + Seçmek + Bölmek + Sil + Sil + Göndermek + %s € + toplam fiyat + İpucu: Gider makbuzlarını sigorta uygulaması aracılığıyla gönderin + Maliyet makbuzlarını sigorta şirketinizin uygulaması aracılığıyla kolayca gönderin. Bir sonraki adımda, bu uygulamayı seçin ve Paylaş\'a basın. + Pratik + Eczane + Tarih + Daha fazla göster + İlaç Kimliği + için verilmiş + KVNR: %s + Doğum tarihi: %s + Tamam + Makbuzları nasıl gönderiyorsunuz? + Doğrudan sigorta şirketinizin/yardım ofisinizin uygulamasına aktarın. Bunu yapmak için sonraki sayfada uygulamayı seçin. + veya + Dosyayı kaydedin ve daha sonra sigorta/yardım portalına aktarın. + Makale: %s + Sayı: %s + KDV: %s %% + EUR cinsinden brüt fiyat: %s + Ek Ücretler + Acil servis ücreti + BTM ücreti + T reçete ücreti + satın alma maliyetleri + Kurye hizmeti + EUR cinsinden toplam: %s + harç + Gerçekten silinsin mi? + Dosya cihazınızdan ve sunucudan silinecektir. + Sil diff --git a/android/src/main/res/values-uk/strings.xml b/android/src/main/res/values-uk/strings.xml index dd21c4c0..3e529ada 100644 --- a/android/src/main/res/values-uk/strings.xml +++ b/android/src/main/res/values-uk/strings.xml @@ -33,14 +33,12 @@ Ввести PIN-код Спробуйте ще раз Помилка з’єднання з сервером - Введено неправильний PIN-код. У вас ще %s спроба, перш ніж буде заблоковано картку У вас ще %s спроби, перш ніж буде заблоковано картку У вас ще %s спроб, перш ніж буде заблоковано картку - Введено неправильний CAN Ви знайдете номер доступу у верхньому правому куті своєї картки здоров\'я. Скасувати Пошук картки... @@ -227,8 +225,8 @@ Помилка встановлення зв’язок із сервером: код статусу %s Помилка встановлення зв’язок із сервером: помилка VAU Попередження - Цьому пристрою, мабуть, не можна повністю довіряти. - З міркувань безпеки цей застосунок не слід використовувати на рутованих пристроях. + Ваш пристрій може мати знижений рівень безпеки + Це може бути спричинено, наприклад, маніпуляціями з пристроями або активованим режимом розробника. З міркувань безпеки ми не рекомендуємо використовувати програму на зламаних пристроях. Я беру до уваги підвищений ризик, та все ж хочу продовжувати. Чому пристрої з root-доступом становлять потенційну загрозу безпеці? Детальніше @@ -439,7 +437,6 @@ Запам\'ятали PIN-код? Занотуйте собі свій PIN-код та зберігайте його в надійному місці. Скасувати - Введено неправильний PUK-код. Ok Розблокування не можливе За допомогою цього PUK-коду ви досягли максимальної кількості розблокувань картки або ще раз ввели його неправильно. Зв’яжіться зі своєю страховою компанією. @@ -571,7 +568,6 @@ Наразі відкрито і біля мене Фільтрувати за… почати пошук - Був викуплений для вас пряме доручення аптеках номер телефону (необов\'язково) @@ -649,8 +645,8 @@ Отримати Що таке пряме доручення? У разі прямого направлення, рецепт від практики або лікарні викуповується безпосередньо в аптеці. Застраховані особи не повинні вживати жодних дій і не можуть втручатися в процес викупу. \n\n Прямі напрямки перераховані в додатку для електронних рецептів, щоб зробити ваше лікування більш прозорим для вас. - Немає плати за екстрену службу - Поспішати тут на порядку денному. Цей рецепт також можна отримати вночі в аптеці без додаткової оплати екстреної допомоги. + Плата за аварійну службу + Іноді потрібно поспішати. Деякі рецепти можна отримати без доплати за екстрену допомогу, наприклад, вночі або у святкові дні. Препарати, що підлягають співоплаті Звільнено від співоплати Ті, хто має державне медичне страхування, повинні сплачувати доплату в розмірі до десяти євро за рецептурні ліки. \n\n Сума співоплати залежить від вартості ваших ліків. За ліки, які коштують менше 5 євро, ви повинні платити самі.\n За ліки, які коштують дорожче, потрібно заплатити десять відсотків від ціни, але не менше 5 євро, а максимум 10 євро. \n\n Діти та молодь віком до 18 років, як правило, звільнені від співоплати. \n\n Якщо ваші річні витрати на ліки перевищують ваш фінансовий ліміт, ви можете бути звільнені від співоплати. Поговоріть про це зі своїм медичним страхувальником. @@ -818,4 +814,58 @@ Ваша страхова компанія пропонує такі способи зв’язку Ваша страхова компанія пропонує такі способи зв’язку Закрити + PIN-код введено неправильно. + Номер доступу введено неправильно + PUK введено неправильно. + видаткові квитанції + Показати видаткові квитанції + видаткові квитанції + Для отримання видаткових квитанцій необхідно підключитися до сервера. + Підключити + Відсутність видаткових квитанцій + Дезактивувати + Скасувати + відключити функцію + Це видалить усі квитанції з цього пристрою та із сервера. + Отримувати видаткові квитанції + Ваші чеки також зберігаються на сервері рецептів. + Отримати + Усього: %s %s + Вибрати + Спліт + Видалити + Видалити + Надіслати + %s євро + Загальна сума + Порада: надсилайте квитанції про витрати через програму страхування + Легко надсилайте квитанції про витрати через додаток вашої страхової компанії. На наступному кроці виберіть цю програму та натисніть «Поділитися». + Практика + Аптека + Дата + Показати більше + ID препарату + Видається за + КВНР: %s + Дата народження: %s + Ok + Як ви подаєте квитанції? + Передайте безпосередньо в додаток вашої страхової компанії/служби допомоги. Для цього виберіть додаток на наступній сторінці. + або + Збережіть файл і пізніше імпортуйте його на портал страхування/допомоги. + Стаття: %s + Номер: %s + ПДВ: %s %% + Ціна брутто в євро: %s + Додаткові збори + Плата за аварійну службу + Комісія BTM + T збір за рецептом + витрати на закупівлю + Кур\'єрська служба + Усього в євро: %s + збір + Справді видалити? + Файл буде видалено з вашого пристрою та з сервера. + Видалити diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 8c4274f9..1a617757 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -31,12 +31,10 @@ PIN eingeben Erneut probieren Verbindung mit dem Server herstellen fehlgeschlagen. - Falsche PIN eingegeben. Sie haben noch %s weiteren Versuch, bevor Ihre Karte gesperrt wird. Sie haben noch %s weitere Versuche, bevor Ihre Karte gesperrt wird. - Falsche CAN eingegeben Sie finden die Zugangsnummer oben rechts auf Ihrer Gesundheitskarte. Abbrechen Suche nach Karte… @@ -228,8 +226,8 @@ Kommunikation mit dem Server fehlgeschlagen: Statuscode %s. Kommunikation mit dem Server fehlgeschlagen: VAU Fehler Warnung - Diesem Gerät darf eventuell nicht voll vertraut werden - Diese App sollte aus Sicherheitsgründen nicht auf gerooteten Geräten genutzt werden. + Ihr Gerät hat möglicherweise eine reduzierte Sicherheit + Diese kann z.B. durch manipulierte Geräte oder einen eingeschalteten Entwicklermodus entstehen. Wir empfehlen, die App aus Sicherheitsgründen nicht auf gejailbreakten Geräten zu nutzen. Ich nehme das erhöhte Risiko zur Kenntnis und möchte dennoch fortfahren. Weshalb sind Geräte mit Root-Zugriff ein potentielles Sicherheitsrisiko? Mehr erfahren @@ -368,7 +366,7 @@ Um sich zu authentifizieren, 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/fragen-antworten + https://www.das-e-rezept-fuer-deutschland.de/faq Hilfebereich Packungsgröße und Einheit Wirkstoff @@ -446,7 +444,6 @@ PIN gemerkt? Bitte notieren Sie sich Ihre PIN und bewahren an einem sicheren Ort auf. Abbrechen - Falsche PUK eingegeben. Okay Entsperrung nicht möglich Sie haben mit dieser PUK die maximale Anzahl an Karten-Entsperrungen erreicht oder haben sie wiederholt falsch eingegeben. Bitte wenden Sie sich an Ihre Versicherung. @@ -500,7 +497,7 @@ Wie schade… Leider erfüllt Ihr Gerät die Mindestanforderungen für die Anmeldung in der E-Rezept-App nicht. Für eine sichere Authentifizierung mit Ihrer Gesundheitskarte werden mindestens Android 7 sowie ein NFC-Chip benötigt. Mehr erfahren - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten + https://www.das-e-rezept-fuer-deutschland.de/faq Zugangsdaten speichern? Speichern Nicht speichern @@ -578,7 +575,8 @@ Aktuell geöffnet und in meiner Nähe Filtern nach … Suche starten - Wurde für Sie eingelöst + Wird für Sie eingelöst + Wurde für Sie eingelöst Direktzuweisung Apotheken Telefonnummer (optional) @@ -658,8 +656,8 @@ Erhalten 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. - Keine Notdienstgebühr - Hier ist Eile geboten. Dieses Rezept kann ohne die zusätzliche Zahlung einer Notdienstgebühr auch nachts in einer Apotheke eingelöst werden. + 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. 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. @@ -825,6 +823,9 @@ Ihre Versicherung bietet folgende Kontaktmöglichkeiten Ihre Versicherung bietet folgende Kontaktmöglichkeit Schließen + PIN falsch eingegeben. + Zugangsnummer falsch eingegeben + PUK falsch eingegeben. Kostenbelege Kostenbelege anzeigen Kostenbelege @@ -838,7 +839,7 @@ Kostenbelege empfangen Ihre Kostenbelege werden zusätzlich auf dem Rezeptserver gespeichert. Empfangen - Gesamt: %s %s + Gesamt: %s %s Auswählen Teilen Löschen @@ -881,6 +882,5 @@ Wirklich löschen? Die Datei wird von Ihrem Gerät und vom Server gelöscht. Löschen - - + Sie müssen angemeldet sein, um diesen Service zu nutzen. diff --git a/android/src/main/res/values/strings_desktop.xml b/android/src/main/res/values/strings_desktop.xml index 501649f8..deb46bfc 100644 --- a/android/src/main/res/values/strings_desktop.xml +++ b/android/src/main/res/values/strings_desktop.xml @@ -56,6 +56,6 @@ https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten + https://www.das-e-rezept-fuer-deutschland.de/faq https://www.das-e-rezept-fuer-deutschland.de/kontakt \ No newline at end of file diff --git a/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt b/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt index 23bc3255..ce659c3b 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt @@ -89,11 +89,19 @@ class PharmacySearchViewModelTest { ) private val pharmacy = PharmacyUseCaseData.Pharmacy( + id = "", name = "Test - Pharmacy", address = null, location = null, distance = null, - contacts = PharmacyContacts(phone = "", mail = "", url = ""), + contacts = PharmacyContacts( + phone = "", + mail = "", + url = "", + pickUpUrl = "", + deliveryUrl = "", + onlineServiceUrl = "" + ), provides = listOf(), openingHours = null, telematikId = "", @@ -124,7 +132,17 @@ class PharmacySearchViewModelTest { ) ) orderState = PharmacyOrderState( - profileId = "0", + profile = ProfilesUseCaseData.Profile( + id = "0", + name = "Test", + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + active = false, + color = ProfilesData.ProfileColorNames.PINK, + lastAuthenticated = null, + ssoTokenScope = null, + personalizedImage = null, + avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage + ), useCase = useCase, scope = CoroutineScope(coroutineRule.dispatchers.Unconfined) ) diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt b/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt index bf267116..32296208 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt @@ -91,7 +91,9 @@ class PrescriptionUseCaseTest { useCase.redeemedPrescriptions( profileId = "", now = LocalDate.parse("2021-02-01").atStartOfDayIn(TimeZone.UTC) - ).first().map { it.taskId } + ).first().map { + it.taskId + } ) } } diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt b/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt index 41395188..d4833e3a 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt @@ -21,9 +21,12 @@ package de.gematik.ti.erp.app.utils import de.gematik.ti.erp.app.fhir.parser.FhirTemporal import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import java.util.UUID import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.parse fun syncedTask( taskId: String = "Task/" + UUID.randomUUID().toString(), @@ -172,6 +175,28 @@ val testSyncedTasks = authoredOn = Instant.parse("2020-12-04T09:49:46Z"), status = SyncedTaskData.TaskStatus.Ready, medicationName = "Viel zu viel" + ), + // 5 + syncedTask( + lastModified = Clock.System.now().plus(10.minutes), + organizationName = "MVZ Haus der vielen Ärzte", + practitionerName = null, + expiresOn = Instant.parse("2020-12-04T09:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-04T09:49:46Z") + 28.days, + authoredOn = Instant.parse("2020-12-04T09:49:46Z"), + status = SyncedTaskData.TaskStatus.Completed, + medicationName = "Viel zu viel" + ), + // 6 + syncedTask( + lastModified = Clock.System.now().plus(1.minutes), + organizationName = "MVZ Haus der vielen Ärzte", + practitionerName = null, + expiresOn = Instant.parse("2020-12-04T09:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-04T09:49:46Z") + 28.days, + authoredOn = Instant.parse("2020-12-04T09:49:46Z"), + status = SyncedTaskData.TaskStatus.Completed, + medicationName = "Viel zu viel" ) ) @@ -238,6 +263,8 @@ val testScannedTasksOrdered by lazy { // keep in sync with `testSyncedTasks` val testRedeemedTasksOrdered = listOf( + testSyncedTasks[5], + testSyncedTasks[6], testSyncedTasks[0], testScannedTasks[0], testSyncedTasks[1] diff --git a/build.gradle.kts b/build.gradle.kts index 58f9f0c3..8f300824 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,14 +13,14 @@ plugins { // generates licence report id("com.jaredsburrows.license") version "0.8.90" apply false - kotlin("multiplatform") version "1.7.20" apply false - kotlin("plugin.serialization") version "1.7.20" apply false + kotlin("multiplatform") version "1.8.10" apply false + 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.6.1" apply false - id("org.jetbrains.kotlin.android") version "1.7.20" apply false + id("io.realm.kotlin") version "1.7.1" apply false + id("org.jetbrains.kotlin.android") version "1.8.10" apply false id("com.android.application") version "7.4.1" apply false id("com.android.library") version "7.4.1" apply false - id("org.jetbrains.compose") version "1.3.0" apply false + id("org.jetbrains.compose") version "1.4.0" apply false id("com.codingfeline.buildkonfig") version "0.13.3" apply false id("io.gitlab.arturbosch.detekt") version "1.22.0" } @@ -72,7 +72,7 @@ fun ktlintCreating(format: Boolean, sources: List, disableLicenceRule: B tasks.creating(JavaExec::class) { description = "Fix Kotlin code style deviations." classpath = ktlintMain + ktlintRules - main = "com.pinterest.ktlint.Main" + mainClass.set("com.pinterest.ktlint.Main") args = mutableListOf().apply { if (format) add("-F") addAll(sources) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 90e36b96..758680f3 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -81,7 +81,7 @@ kotlin { android() jvm("desktop") { compilations.all { - kotlinOptions.jvmTarget = "15" + kotlinOptions.jvmTarget = "17" } } sourceSets { 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 819345f6..77e06364 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 @@ -29,7 +29,7 @@ interface PharmacyRedeemService { @POST @Headers("Content-Type: application/pkcs7-mime") - suspend fun redeem( + suspend fun redeemDirectly( @Url url: String, @Body message: RequestBody ): Response 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 a6eb935c..6207e1d7 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 @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asFhirLocalDate 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.containedArrayOrNull @@ -29,6 +28,7 @@ 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.fhir.parser.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive @@ -167,7 +167,7 @@ fun JsonElement.extractMultiplePresc val start = this.findAll("extension").filterWith("url", stringValue("Zeitraum")) .firstOrNull() ?.contained("valuePeriod") - ?.containedOrNull("start")?.jsonPrimitive?.asFhirLocalDate() + ?.containedOrNull("start")?.jsonPrimitive?.toFhirTemporal() return processMultiplePrescriptionInfo( indicator, 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 944044fc..1e864cf8 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 @@ -33,7 +33,6 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive -import kotlinx.datetime.Instant private fun template( orderId: String, @@ -76,7 +75,7 @@ private fun template( } """.trimIndent() -// private fun templateVersion12( +// private fun templateVersion1_2( // orderId: String, // reference: String, // payload: String, 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 de95ee0f..d2f2da24 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 @@ -29,3 +29,18 @@ data class CommunicationPayload( val hint: String = "", val phone: String? ) + +@Serializable +data class DirectCommunicationMessage( + val version: String = "2", + val supplyOptionsType: String, + val name: String, + val address: List, + val hint: String = "", + val text: String?, + val phone: String?, + val mail: String?, + val transactionID: String, + val taskID: String, + val accessCode: String +) 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 8d15e164..35f70b4c 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 @@ -74,7 +74,7 @@ typealias MedicationRequestFn = ( typealias MultiplePrescriptionInfoFn = ( indicator: Boolean, numbering: Ratio?, - start: FhirTemporal.LocalDate? + start: FhirTemporal? ) -> R typealias MedicationFn = ( 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 9c6163ca..269fb56a 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 @@ -18,10 +18,12 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.fhir.parser.containedArray import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull 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.containedString import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull @@ -41,6 +43,10 @@ import kotlinx.datetime.DayOfWeek val Contained = listOf("contained") val TypeCodingCode = listOf("type", "coding", "code") +const val PickUpRank = 100 +const val DeliveryRank = 200 +const val OnlineServiceRank = 300 + /** * Extract pharmacy services from a search bundle. */ @@ -52,16 +58,17 @@ fun extractPharmacyServices( val bundleTotal = bundle.containedInt("total") val resources = bundle.findAll(listOf("entry", "resource")).filterWith("name", not(stringValue("-"))) - val pharmacies = resources.mapCatching(onError) { resource -> - val locationName = resource.containedString("name") + val pharmacies = resources.mapCatching(onError) { pharmacy -> + val locationId = pharmacy.containedString("id") + val locationName = pharmacy.containedString("name") val localService = LocalPharmacyService( name = locationName, - openingHours = resource.containedArrayOrNull("hoursOfOperation")?.let { hoursOfOperation(it) } + openingHours = pharmacy.containedArrayOrNull("hoursOfOperation")?.let { hoursOfOperation(it) } ?: OpeningHours(emptyMap()) ) val deliveryPharmacyService = - resource + pharmacy .findAll(Contained) .filterWith(TypeCodingCode, stringValue("498")) .firstOrNull() @@ -87,7 +94,7 @@ fun extractPharmacyServices( // } val telematikId = - resource + pharmacy .findAll(listOf("identifier")) .filterWith( listOf("system"), @@ -102,7 +109,7 @@ fun extractPharmacyServices( var isOutpatientPharmacy = false var isMobilePharmacy = false - resource.findAll(TypeCodingCode).forEach { + pharmacy.findAll(TypeCodingCode).forEach { when (it.containedString()) { "OUTPHARM" -> isOutpatientPharmacy = true "MOBL" -> isMobilePharmacy = true @@ -121,7 +128,7 @@ fun extractPharmacyServices( null } - val position = resource.containedObject("position").let { + val position = pharmacy.containedObject("position").let { Location( latitude = it.containedDouble("latitude"), longitude = it.containedDouble("longitude") @@ -129,16 +136,24 @@ fun extractPharmacyServices( } Pharmacy( + id = locationId, name = locationName, location = position, - address = resource.containedObject("address").let { address -> + address = pharmacy.containedObject("address").let { address -> PharmacyAddress( lines = address.containedArray("line").map { it.containedString() }, - postalCode = address.containedString("postalCode"), + postalCode = if (BuildKonfig.INTERNAL) { + address.containedStringOrNull("postalCode") ?: "" + } else { + address.containedString("postalCode") + }, city = address.containedString("city") ) }, - contacts = resource.containedArrayOrNull("telecom")?.let { contacts(it) } ?: PharmacyContacts( + contacts = pharmacy.containedArrayOrNull("telecom")?.let { contacts(it) } ?: PharmacyContacts( + "", + "", + "", "", "", "" @@ -150,7 +165,7 @@ fun extractPharmacyServices( pickUpPharmacyService ), telematikId = telematikId, - ready = resource.containedString("status") == "active" + ready = pharmacy.containedString("status") == "active" ) } @@ -164,14 +179,16 @@ fun extractPharmacyServices( /** * Extract certificates from binary bundle. */ -fun extractBinaryCertificateAsBase64( +fun extractBinaryCertificatesAsBase64( bundle: JsonElement -): String { - val resource = bundle.findAll(listOf("entry", "resource")).first() - - require(resource.containedString("contentType") == "application/pkix-cert") - - return resource.containedString("data") +): List { + val resources = bundle.findAll(listOf("entry", "resource")) + val resourceStrings = mutableListOf() + for (resource in resources) { + require(resource.containedString("contentType") == "application/pkix-cert") + resourceStrings.add(resource.containedString("data")) + } + return resourceStrings.toList() } private fun Sequence.mapCatching( @@ -204,6 +221,9 @@ private fun contacts( var phone = "" var mail = "" var url = "" + var pickUpUrl = "" + var deliveryUrl = "" + var onlineServiceUrl = "" telecom .forEach { @@ -211,13 +231,21 @@ private fun contacts( "phone" -> phone = it.containedStringOrNull("value") ?: "" "email" -> mail = it.containedStringOrNull("value") ?: "" "url" -> url = sanitizeUrl(it.containedStringOrNull("value") ?: "") + "other" -> when (it.containedIntOrNull("rank")) { + PickUpRank -> pickUpUrl = sanitizeUrl(it.containedStringOrNull("value") ?: "") + DeliveryRank -> deliveryUrl = sanitizeUrl(it.containedStringOrNull("value") ?: "") + OnlineServiceRank -> onlineServiceUrl = sanitizeUrl(it.containedStringOrNull("value") ?: "") + } } } return PharmacyContacts( phone = phone, mail = mail, - url = url + url = url, + pickUpUrl = pickUpUrl, + deliveryUrl = deliveryUrl, + onlineServiceUrl = onlineServiceUrl ) } 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 6df6c1f2..5785431e 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 @@ -80,10 +80,14 @@ data class PharmacyAddress( data class PharmacyContacts( val phone: String, val mail: String, - val url: String + val url: String, + val pickUpUrl: String, + val deliveryUrl: String, + val onlineServiceUrl: String ) data class Pharmacy( + val id: String, val name: String, val address: PharmacyAddress, val location: Location, 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 1456031b..a61eac75 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 @@ -276,7 +276,7 @@ class InvoiceLocalDataSource( MultiplePrescriptionInfoEntityV1().apply { this.indicator = indicator this.numbering = numbering - this.start = start?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() + this.start = start?.toInstant(TimeZone.UTC)?.toRealmInstant() } }, processMedicationRequest = { 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 7a1f3192..0815536e 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 @@ -243,7 +243,7 @@ class TaskLocalDataSource( MultiplePrescriptionInfoEntityV1().apply { this.indicator = indicator this.numbering = numbering - this.start = start?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() + this.start = start?.toInstant(TimeZone.UTC)?.toRealmInstant() } }, processMedicationRequest = { 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 5d5dfe12..d2ecf37f 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 @@ -88,7 +88,7 @@ class SettingsRepository constructor( } }.flowOn(dispatchers.IO) - override val pharmacySearch: Flow + override val pharmacySearch: Flow // TODO move to PharmacySearch get() = settings.mapNotNull { settings -> settings?.pharmacySearch?.let { SettingsData.PharmacySearch( 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 ef0b2b29..331d6aa3 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 @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.fhir.model -import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals @@ -174,7 +173,7 @@ class CommonRessourceMapperTest { processMultiplePrescriptionInfo = { indicator, numbering, start -> assertEquals(true, indicator) assertEquals(ReturnType.Ratio, numbering) - assertEquals(LocalDate.parse("2022-08-17"), start?.value) + assertEquals("2022-08-17", start?.formattedString()) ReturnType.MultiplePrescriptionInfo } ) 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 c7e382d0..31137fec 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 @@ -27,12 +27,16 @@ import kotlin.test.assertEquals private val testBundle by lazy { File("$ResourceBasePath/pharmacy_result_bundle.json").readText() } private val testBundleBinaries by lazy { File("$ResourceBasePath/fhir/pharmacy_binary.json").readText() } +private val directRedeemPharmacyBundle by lazy { + File("$ResourceBasePath/direct_redeem_pharmacy_bundle.json").readText() +} class PharmacyMapperTest { private val openingTimeA = OpeningTime(LocalTime.parse("08:00:00"), LocalTime.parse("12:00:00")) private val openingTimeB = OpeningTime(LocalTime.parse("14:00:00"), LocalTime.parse("18:00:00")) private val openingTimeC = OpeningTime(LocalTime.parse("08:00:00"), LocalTime.parse("20:00:00")) private val expected = Pharmacy( + id = "4b74c2b2-2275-4153-a94d-3ddc6bfb1362", name = "Heide-Apotheke", address = PharmacyAddress( lines = listOf("Langener Landstraße 266"), @@ -43,7 +47,10 @@ class PharmacyMapperTest { contacts = PharmacyContacts( phone = "0471/87029", mail = "info@heide-apotheke-bremerhaven.de", - url = "http://www.heide-apotheke-bremerhaven.de" + url = "http://www.heide-apotheke-bremerhaven.de", + pickUpUrl = "", + deliveryUrl = "", + onlineServiceUrl = "" ), provides = listOf( LocalPharmacyService( @@ -82,6 +89,38 @@ class PharmacyMapperTest { ready = true ) + private val expectedDirectRedeemPharmacy = + Pharmacy( + id = "ngc26fe2-9c3a-4d52-854e-794c96f73f66", + name = "PT-STA-Apotheke 2TEST-ONLY", + address = PharmacyAddress( + lines = listOf("Münchnerstr. 15 b"), + postalCode = "82139", + city = "Starnberg" + ), + location = Location(latitude = 48.0018513, longitude = 11.3497755), + contacts = PharmacyContacts( + phone = "", + mail = "", + url = "", + pickUpUrl = "https://ixosapi.service-pt.de/api/GematikAppZuweisung/945357?sig=Co" + + "LEeMyykSQul06Rp4wyTsfOJPBrSHOG2YBB4Bzy8QQ%3d&se=2625644988", + deliveryUrl = "https://ixosapi.service-pt.de/api/GematikAppZuweisung/945357?sig=CoLEeM" + + "yykSQul06Rp4wyTsfOJPBrSHOG2YBB4Bzy8QQ%3d&se=2625644988", + onlineServiceUrl = "" + ), + provides = listOf( + LocalPharmacyService( + name = "PT-STA-Apotheke 2TEST-ONLY", + openingHours = OpeningHours(mapOf()) + ), + OnlinePharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY"), + PickUpPharmacyService(name = "PT-STA-Apotheke 2TEST-ONLY") + ), + telematikId = "3-SMC-B-Testkarte-883110000116948", + ready = true + ) + @Test fun `map pharmacies from JSON bundle`() { val pharmacies = extractPharmacyServices( @@ -97,9 +136,24 @@ class PharmacyMapperTest { assertEquals(expected, pharmacies[0]) } + @Test + fun `map direct redeem pharmacy from JSON bundle get urls for direct redeem`() { + val pharmacies = extractPharmacyServices( + Json.parseToJsonElement(directRedeemPharmacyBundle), + onError = { element, cause -> + println(element) + throw cause + } + ).pharmacies + + assertEquals(1, pharmacies.size) + + assertEquals(expectedDirectRedeemPharmacy, pharmacies[0]) + } + @Test fun `extract certificate`() { - val result = extractBinaryCertificateAsBase64(Json.parseToJsonElement(testBundleBinaries)) - assertEquals("MIIFlDCCBHygAwwKGi44czSg==", result) + val result = extractBinaryCertificatesAsBase64(Json.parseToJsonElement(testBundleBinaries)) + assertEquals(listOf("MIIFlDCCBHygAwwKGi44czSg=="), result) } } diff --git a/common/src/commonTest/resources/direct_redeem_pharmacy_bundle.json b/common/src/commonTest/resources/direct_redeem_pharmacy_bundle.json new file mode 100644 index 00000000..5b571add --- /dev/null +++ b/common/src/commonTest/resources/direct_redeem_pharmacy_bundle.json @@ -0,0 +1,100 @@ +{ + "id": "8753012b-5b30-4405-ab97-05ed076583bf", + "meta": { + "lastUpdated": "2023-03-27T17:19:41.773654036+02:00" + }, + "resourceType": "Bundle", + "type": "searchset", + "total": 1, + "link": [ + { + "relation": "self", + "url": "Bundle8753012b-5b30-4405-ab97-05ed076583bf" + } + ], + "entry": [ + { + "resource": { + "id": "ngc26fe2-9c3a-4d52-854e-794c96f73f66", + "resourceType": "Location", + "address": { + "use": "work", + "type": "physical", + "line": [ + "Münchnerstr. 15 b" + ], + "postalCode": "82139", + "city": "Starnberg", + "country": "de" + }, + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-SMC-B-Testkarte-883110000116948" + } + ], + "name": "PT-STA-Apotheke 2TEST-ONLY", + "position": { + "latitude": 48.0018513, + "longitude": 11.3497755 + }, + "status": "active", + "telecom": [ + { + "system": "other", + "value": "https://ixosapi.service-pt.de/api/GematikAppZuweisung/945357?sig=CoLEeMyykSQul06Rp4wyTsfOJPBrSHOG2YBB4Bzy8QQ%3d&se=2625644988", + "use": "mobile", + "rank": 100 + }, + { + "system": "other", + "value": "https://ixosapi.service-pt.de/api/GematikAppZuweisung/945357?sig=CoLEeMyykSQul06Rp4wyTsfOJPBrSHOG2YBB4Bzy8QQ%3d&se=2625644988", + "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" + } + ] + }, + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code": "MOBL", + "display": "Mobile Services" + } + ] + } + ] + }, + "search": { + "mode": "match" + } + } + ] +} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 91ae4846..82dcdf93 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -58,7 +58,7 @@ fun networkConfigPath(name: String): String { kotlin { jvm { compilations.all { - kotlinOptions.jvmTarget = "15" + kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } withJava() 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 index 85521110..8a17777b 100644 --- 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 @@ -34,7 +34,6 @@ 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.PointerIconDefaults import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerMoveFilter import androidx.compose.ui.layout.onSizeChanged @@ -63,7 +62,7 @@ fun HorizontalSplittable( if (hovered || dragging) { PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)) } else { - PointerIconDefaults.Default + PointerIcon.Default } } 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 index d7c482c9..d77df7c3 100644 --- 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 @@ -19,6 +19,7 @@ package de.gematik.ti.erp.app.idp.usecase import de.gematik.ti.erp.app.BCProvider +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 diff --git a/gradle.properties b/gradle.properties index b11d87fe..afe5b818 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ buildkonfig.flavor=googleTuInternal # VERSION_CODE=1 VERSION_NAME=1.0 -USER_AGENT=eRp-App-Android/1.10.0 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.11.0 GMTIK/eRezeptApp # DATA_PROTECTION_LAST_UPDATED = 2022-01-06 # diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d9e2cc7a..5f5824b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Nov 09 23:18:41 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/plugins/dependencies/build.gradle.kts b/plugins/dependencies/build.gradle.kts index bf0e5dfa..a0bb10e3 100644 --- a/plugins/dependencies/build.gradle.kts +++ b/plugins/dependencies/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } tasks.withType() { - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = "17" } gradlePlugin { 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 4de34650..b6b98c0c 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 @@ -37,7 +37,7 @@ class AppDependenciesPlugin : Plugin { project.plugins.all { if (this is AppPlugin) { project.extensions.getByType(BaseAppModuleExtension::class).apply { - composeOptions.kotlinCompilerExtensionVersion = "1.3.2" + composeOptions.kotlinCompilerExtensionVersion = "1.4.6" buildFeatures { compose = true } @@ -180,10 +180,10 @@ class AppDependenciesPlugin : Plugin { } object Database { - const val realm = "io.realm.kotlin:library-base:1.6.1" + const val realm = "io.realm.kotlin:library-base:1.7.1" } - const val composeVersion = "1.3.+" + const val composeVersion = "1.4.2" object Compose { const val compiler = "androidx.compose.compiler:compiler:$composeVersion" diff --git a/plugins/resource-generation/build.gradle.kts b/plugins/resource-generation/build.gradle.kts index 7100150f..03f93fe8 100644 --- a/plugins/resource-generation/build.gradle.kts +++ b/plugins/resource-generation/build.gradle.kts @@ -2,13 +2,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` - kotlin("jvm") version "1.7.0" - kotlin("plugin.serialization") version "1.7.0" + kotlin("jvm") version "1.8.0" + kotlin("plugin.serialization") version "1.8.0" `java-gradle-plugin` } tasks.withType() { - kotlinOptions.jvmTarget = "15" + kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } diff --git a/rules/build.gradle.kts b/rules/build.gradle.kts index 2ef802b4..869f2ee4 100644 --- a/rules/build.gradle.kts +++ b/rules/build.gradle.kts @@ -13,7 +13,7 @@ version = 1.0 group = "de.gematik.ti.erp.app" tasks.withType() { - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } diff --git a/smartcard-wrapper/build.gradle.kts b/smartcard-wrapper/build.gradle.kts index bf3293b3..2943363e 100644 --- a/smartcard-wrapper/build.gradle.kts +++ b/smartcard-wrapper/build.gradle.kts @@ -8,7 +8,7 @@ version = 1.0 group = "de.gematik.ti.erp.app" tasks.withType { - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = "17" kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" }