From 87ff7e6c6a7ba326d7c71a63f07145f335d65f07 Mon Sep 17 00:00:00 2001 From: Gematik Date: Tue, 14 Feb 2023 10:19:48 +0100 Subject: [PATCH] Release 1.8.0 --- README.md | 4 +- ReleaseNotes.md | 4 + android/build.gradle.kts | 10 +- .../ti/erp/app/debug/ui/DebugScreen.kt | 41 +- android/src/main/AndroidManifest.xml | 1 + android/src/main/assets/data_terms.html | 451 +++------------- .../main/java/de/gematik/ti/erp/app/App.kt | 3 + .../de/gematik/ti/erp/app/MainActivity.kt | 4 +- .../java/de/gematik/ti/erp/app/TestTags.kt | 22 + .../gematik/ti/erp/app/analytics/Analytics.kt | 107 ++-- .../cardunlock/model/UnlockEgkNavigation.kt | 5 +- .../cardunlock/ui/UnlockEGKTroubleshooting.kt | 167 ------ .../app/cardunlock/ui/UnlockEgKComponents.kt | 136 ++--- .../erp/app/cardunlock/ui/UnlockEgkDialog.kt | 32 +- .../app/cardwall/mini/ui/HealthCardPrompt.kt | 6 +- .../cardwall/mini/ui/SecureHardwarePrompt.kt | 2 +- .../erp/app/cardwall/ui/CardWallAnimation.kt | 338 ++++++++++++ .../erp/app/cardwall/ui/CardWallAuthDialog.kt | 503 ++++-------------- .../erp/app/cardwall/ui/CardWallComponents.kt | 126 ++--- ...WallViewModel.kt => CardWallController.kt} | 33 +- .../ui/CardWallNfcInstructionScreen.kt | 6 +- .../cardwall/ui/CardWallTroubleshooting.kt | 139 ----- .../erp/app/cardwall/ui/model/Navigation.kt | 26 +- .../gematik/ti/erp/app/core/AppScopedCache.kt | 22 + .../gematik/ti/erp/app/core/MainViewModel.kt | 16 +- .../de/gematik/ti/erp/app/di/AllModules.kt | 2 +- .../de/gematik/ti/erp/app/di/RealmModule.kt | 6 +- .../app/featuretoggle/FeatureToggleManager.kt | 3 +- .../mainscreen/ui/DataTermsUpdateScreen.kt | 135 ----- .../app/mainscreen/ui/MainScreenComponents.kt | 396 ++++++++------ .../ti/erp/app/mainscreen/ui/ProfileChips.kt | 9 + .../ti/erp/app/mainscreen/ui/ToolTip.kt | 1 - .../app/onboarding/ui/OnboardingComponents.kt | 187 ++++--- .../repository/CommunicationRepository.kt | 4 +- .../ti/erp/app/orders/ui/MessageSheets.kt | 32 +- .../ti/erp/app/orders/ui/OrderScreen.kt | 20 +- .../orders/usecase/model/OrderUseCaseData.kt | 2 +- .../ti/erp/app/pharmacy/model/PharmacyData.kt | 2 +- .../repository/PharmacyLocalDataSource.kt | 6 +- .../gematik/ti/erp/app/pharmacy/ui/Details.kt | 13 +- .../pharmacy/ui/EditShippingContactScreen.kt | 301 +++++------ .../ti/erp/app/pharmacy/ui/MapsOverview.kt | 21 +- .../erp/app/pharmacy/ui/PharmacyOrderState.kt | 56 +- .../pharmacy/ui/PharmacySearchController.kt | 12 +- .../app/pharmacy/ui/PharmacySearchScreen.kt | 121 +++-- .../pharmacy/ui/model/ContactInputFields.kt | 196 +++++++ .../pharmacy/usecase/PharmacySearchUseCase.kt | 2 +- .../usecase/model/PharmacyUseCaseData.kt | 2 +- .../detail/ui/AccidentInformation.kt | 18 +- .../app/prescription/detail/ui/InfoSheet.kt | 9 +- .../detail/ui/MedicationOverviewScreen.kt | 1 + .../detail/ui/OrganizationScreen.kt | 1 + .../prescription/detail/ui/PatientScreen.kt | 13 +- .../detail/ui/PrescriberScreen.kt | 1 + .../detail/ui/PrescriptionDetailScreen.kt | 216 +++----- .../detail/ui/SharePrescriptionController.kt | 4 +- .../detail/ui/SyncedMedicationDetailScreen.kt | 22 +- .../detail/ui/TechnicalInformation.kt | 1 + .../detail/ui/model/PrescriptionData.kt | 2 +- .../repository/LocalDataSource.kt | 4 +- .../repository/PrescriptionRepository.kt | 2 +- .../erp/app/prescription/ui/ArchiveScreen.kt | 16 +- .../ui/PrescriptionScreenComponents.kt | 176 +----- .../ui/ScanPrescriptionsViewModel.kt | 6 +- .../ti/erp/app/prescription/ui/StatusChip.kt | 14 +- .../app/prescription/ui/TwoDCodeValidator.kt | 2 +- .../prescription/ui/model/SentOrCompleted.kt | 7 +- .../usecase/PrescriptionUseCase.kt | 9 +- .../usecase/model/PrescriptionUseCaseData.kt | 2 +- .../gematik/ti/erp/app/profiles/ui/Avatar.kt | 8 +- .../erp/app/profiles/ui/EditProfileScreen.kt | 49 +- .../ti/erp/app/profiles/ui/PairedDevices.kt | 20 +- .../ti/erp/app/profiles/ui/ProfileHandler.kt | 13 +- .../app/profiles/usecase/ProfilesUseCase.kt | 17 +- .../ProfilesWithPairedDevicesUseCase.kt | 4 +- .../usecase/model/ProfilesUseCaseData.kt | 26 +- .../erp/app/redeem/usecase/RedeemUseCase.kt | 4 +- .../erp/app/settings/ui/AuditEventsScreen.kt | 13 +- .../erp/app/settings/ui/SettingsViewModel.kt | 2 +- .../app/settings/usecase/SettingsUseCase.kt | 15 +- .../TroubleshootingContent.kt | 98 +++- .../de/gematik/ti/erp/app/utils/DateTime.kt | 68 ++- .../ti/erp/app/utils/compose/Common.kt | 121 ++++- .../erp/app/utils/compose/TimeDescription.kt | 21 +- .../de/gematik/ti/erp/app/vau/VauModule.kt | 7 +- .../src/main/res/drawable-xhdpi/ic_info.webp | Bin 21770 -> 0 bytes .../main_screen_erx_icon_gray_small.webp | Bin 1274 -> 0 bytes .../main_screen_erx_icon_small.webp | Bin 2522 -> 0 bytes .../main/res/drawable-xhdpi/my_location.webp | Bin 6110 -> 0 bytes .../drawable-xhdpi/pharmacist_circle_red.webp | Bin 21142 -> 0 bytes .../src/main/res/drawable-xxhdpi/ic_info.webp | Bin 46286 -> 0 bytes .../main_screen_erx_icon_gray_small.webp | Bin 1648 -> 0 bytes .../main_screen_erx_icon_small.webp | Bin 3874 -> 0 bytes .../main/res/drawable-xxhdpi/my_location.webp | Bin 9478 -> 0 bytes .../pharmacist_circle_red.webp | Bin 40246 -> 0 bytes .../main/res/drawable-xxxhdpi/ic_info.webp | Bin 72884 -> 0 bytes .../main_screen_erx_icon_gray_small.webp | Bin 1946 -> 0 bytes .../main_screen_erx_icon_small.webp | Bin 5394 -> 0 bytes .../res/drawable-xxxhdpi/my_location.webp | Bin 13838 -> 0 bytes .../pharmacist_circle_red.webp | Bin 63682 -> 0 bytes android/src/main/res/values-ar/strings.xml | 230 ++++---- android/src/main/res/values-en/strings.xml | 214 +++++--- android/src/main/res/values-pl/strings.xml | 220 ++++---- android/src/main/res/values-ru/strings.xml | 222 ++++---- android/src/main/res/values-tr/strings.xml | 216 ++++---- android/src/main/res/values-uk/strings.xml | 234 ++++---- android/src/main/res/values/strings.xml | 138 +---- .../ti/erp/app/core/MainViewModelTest.kt | 30 -- .../app/orders/usecase/OrderUseCaseTest.kt | 18 +- .../app/pharmacy/ui/PharmacyControllerTest.kt | 12 +- .../prescription/ui/TwoDCodeValidatorTest.kt | 17 +- .../ui/model/SentOrCompletedTest.kt | 37 +- .../usecase/PrescriptionUseCaseTest.kt | 9 +- .../settings/usecase/SettingsUseCaseTest.kt | 48 +- .../gematik/ti/erp/app/utils/DateTimeTest.kt | 13 +- .../de/gematik/ti/erp/app/utils/TestData.kt | 24 +- .../de/gematik/ti/erp/app/vau/TestData.kt | 12 +- .../erp/app/vau/TruststoreIntegrationTest.kt | 8 +- .../gematik/ti/erp/app/vau/TruststoreTest.kt | 39 +- common/build.gradle.kts | 4 + .../erp/app/fhir/parser/YearMonthAndroid.kt | 22 +- .../gematik/ti/erp/app/api/ResourcePaging.kt | 12 +- .../de/gematik/ti/erp/app/db/Migrations.kt | 12 +- .../ti/erp/app/db/RealmInstantConverter.kt | 31 +- .../ti/erp/app/db/entities/Delegates.kt | 15 +- .../ti/erp/app/db/entities/v1/Profile.kt | 11 + .../ti/erp/app/db/entities/v1/Settings.kt | 4 +- .../erp/app/db/entities/v1/task/Medication.kt | 5 +- .../ti/erp/app/db/entities/v1/task/Patient.kt | 10 +- .../ti/erp/app/fhir/model/AuditEventMapper.kt | 10 +- .../erp/app/fhir/model/CommonPharmacyTimes.kt | 107 ++-- .../app/fhir/model/CommonRessourceMapper.kt | 4 +- .../erp/app/fhir/model/CommunicationMapper.kt | 61 ++- .../ti/erp/app/fhir/model/KBVMapper.kt | 12 +- .../fhir/model/MedicationDispenseMapper.kt | 9 +- .../ti/erp/app/fhir/model/PharmacyMapper.kt | 15 +- .../erp/app/fhir/model/PharmacySearchModel.kt | 44 +- .../fhir/model/ResourceMapperVersion_1_0_2.kt | 23 +- .../fhir/model/ResourceMapperVersion_1_1_0.kt | 24 +- .../ti/erp/app/fhir/model/TaskMapper.kt | 85 +-- .../ti/erp/app/fhir/parser/Converter.kt | 81 --- .../erp/app/fhir/parser/TemporalConverter.kt | 137 +++++ .../ti/erp/app/fhir/parser/YearMonth.kt | 68 +++ .../gematik/ti/erp/app/idp/model/IdpData.kt | 11 +- .../erp/app/idp/repository/IdpRepository.kt | 6 +- .../ti/erp/app/idp/usecase/IdpBasicUseCase.kt | 19 +- .../app/prescription/model/ScannedTaskData.kt | 2 +- .../app/prescription/model/SyncedTaskData.kt | 76 +-- .../repository/TaskLocalDataSource.kt | 36 +- .../prescription/repository/TaskRepository.kt | 4 +- .../ti/erp/app/profiles/model/ProfilesData.kt | 4 +- .../profiles/repository/ProfilesRepository.kt | 12 +- .../erp/app/protocol/model/AuditEventData.kt | 2 +- .../repository/AuditEventLocalDataSource.kt | 2 +- .../repository/AuditEventsRepository.kt | 3 +- .../ti/erp/app/settings/GeneralSettings.kt | 7 +- .../ti/erp/app/settings/model/SettingsData.kt | 2 - .../settings/repository/SettingsRepository.kt | 4 +- .../de/gematik/ti/erp/app/vau/CertUtils.kt | 10 +- .../de/gematik/ti/erp/app/vau/OCSPUtils.kt | 7 +- .../erp/app/vau/usecase/TruststoreConfig.kt | 5 +- .../erp/app/vau/usecase/TruststoreUseCase.kt | 4 +- .../ti/erp/app/api/ResourcePagingTest.kt | 6 +- .../erp/app/db/RealmInstantConverterTest.kt | 38 +- .../ti/erp/app/db/entities/DelegatesTest.kt | 15 +- .../app/fhir/model/AuditEventMapperTest.kt | 35 +- .../fhir/model/CommonRessourceMapperTest.kt | 4 +- .../app/fhir/model/CommunicationMapperTest.kt | 38 +- .../model/MedicationDispenseMapperTest.kt | 118 +++- .../erp/app/fhir/model/PharmacyMapperTest.kt | 2 +- .../model/RessourceMapperVersion102Test.kt | 36 +- .../model/RessourceMapperVersion110Test.kt | 7 +- .../ti/erp/app/fhir/model/TaskMapperTest.kt | 31 +- .../erp/app/fhir/model/TestDataKBVMapper.kt | 17 + .../ti/erp/app/fhir/parser/ConverterTest.kt | 66 --- .../app/fhir/parser/TemporalConverterTest.kt | 51 ++ .../app/idp/repository/IdpRepositoryTest.kt | 6 +- .../app/idp/usecase/IdpBasicUseCaseTest.kt | 12 +- .../repository/ProfilesRepositoryTest.kt | 4 +- .../repository/SettingsRepositoryTest.kt | 12 +- .../gematik/ti/erp/app/vau/OCSPUtilsTest.kt | 12 +- .../de/gematik/ti/erp/app/vau/TestData.kt | 12 +- .../audit_events_bundle_version_1_2.json | 84 +++ .../communications_bundle_version_1_2.json | 74 +++ .../fhir/bundle_med_dispense_version_1_2.json | 249 +++++++++ .../medication_dispense_without_category.json | 94 ++++ .../fhir/patient_incomplete_birth_date.json | 87 +++ .../resources/fhir/task_bundle_vers_1_2.json | 101 ++++ .../resources/fhir/task_vers_1_2.json | 24 +- config/detekt/baseline.xml | 9 - gradle.properties | 52 +- .../gematik/ti/erp/AppDependenciesPlugin.kt | 9 + .../kotlin/de/gematik/ti/erp/Overriding.kt | 6 +- 193 files changed, 4835 insertions(+), 3984 deletions(-) delete mode 100644 android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEGKTroubleshooting.kt create mode 100644 android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt rename android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/{CardWallViewModel.kt => CardWallController.kt} (83%) delete mode 100644 android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallTroubleshooting.kt delete mode 100644 android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataTermsUpdateScreen.kt create mode 100644 android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt rename android/src/main/java/de/gematik/ti/erp/app/{cardwall/ui => troubleShooting}/TroubleshootingContent.kt (73%) delete mode 100644 android/src/main/res/drawable-xhdpi/ic_info.webp delete mode 100644 android/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_small.webp delete mode 100644 android/src/main/res/drawable-xhdpi/main_screen_erx_icon_small.webp delete mode 100644 android/src/main/res/drawable-xhdpi/my_location.webp delete mode 100644 android/src/main/res/drawable-xhdpi/pharmacist_circle_red.webp delete mode 100644 android/src/main/res/drawable-xxhdpi/ic_info.webp delete mode 100644 android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_small.webp delete mode 100644 android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_small.webp delete mode 100644 android/src/main/res/drawable-xxhdpi/my_location.webp delete mode 100644 android/src/main/res/drawable-xxhdpi/pharmacist_circle_red.webp delete mode 100644 android/src/main/res/drawable-xxxhdpi/ic_info.webp delete mode 100644 android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_gray_small.webp delete mode 100644 android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_small.webp delete mode 100644 android/src/main/res/drawable-xxxhdpi/my_location.webp delete mode 100644 android/src/main/res/drawable-xxxhdpi/pharmacist_circle_red.webp rename android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallData.kt => common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt (66%) create mode 100644 common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverter.kt create mode 100644 common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt create mode 100644 common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt create mode 100644 common/src/commonTest/resources/audit_events_bundle_version_1_2.json create mode 100644 common/src/commonTest/resources/communications_bundle_version_1_2.json create mode 100644 common/src/commonTest/resources/fhir/bundle_med_dispense_version_1_2.json create mode 100644 common/src/commonTest/resources/fhir/medication_dispense_without_category.json create mode 100644 common/src/commonTest/resources/fhir/patient_incomplete_birth_date.json create mode 100644 common/src/commonTest/resources/fhir/task_bundle_vers_1_2.json diff --git a/README.md b/README.md index 68e1d691..fa3c595b 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ The official gematik E-Rezept App (electronic prescription app) is available to [![Download E-Rezept on the App Store](https://user-images.githubusercontent.com/52454541/126137060-cb8c7ceb-6a72-423d-9079-f3e1a98b2638.png)](https://apps.apple.com/de/app/das-e-rezept/id1511792179)[![Download E-Rezept on the PlayStore](https://user-images.githubusercontent.com/52454541/126138350-a52e1d84-1588-4e8a-86df-189ee4df8bc8.png)](https://play.google.com/store/apps/details?id=de.gematik.ti.erp.app)[![Download E-Rezept on the App Gallery](https://user-images.githubusercontent.com/52454541/126158983-15d73f12-36c6-41ce-8de5-29d10baaed04.png)](https://appgallery.huawei.com/#/app/C104463531) -and login with the health card of the public health insurance. In July 2021, the e-prescription will start with a test phase, initially in the focus region Berlin-Brandenburg. The nationwide rollout will follow three month later in the fourth quarter. +and login with the health card of the public health insurance. In July 2021, the e-prescription started with a test phase, initially in the focus region Berlin-Brandenburg. The nationwide rollout started three month later in September 2022. The e-prescriptions are stored in the telematics infrastructure, for which gematik is responsible. -Visit our [FAQ page](https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten) for more information about the e-prescription. +Visit our [FAQ page](https://www.das-e-rezept-fuer-deutschland.de/faq) for more information about the e-prescription. ### Support & Feedback diff --git a/ReleaseNotes.md b/ReleaseNotes.md index f214eaba..7dfe581d 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,7 @@ +# Release 1.8.0 +- Switched to new analytics tool +- Bugfixes + # Release 1.7.0 - New wizard for ordering a healthcard - Support for new FHIR profile version diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 224846fb..3dc35ca8 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -210,6 +210,8 @@ dependencies { implementation(coroutines("core")) implementation(coroutines("android")) implementation(coroutines("play-services")) + compileOnly(datetime) + testCompileOnly(datetime) } android { coreLibraryDesugaring(desugaring) @@ -293,6 +295,11 @@ dependencies { passwordStrength { implementation(zxcvbn) } + + contentSquare { + implementation(cts) + } + playServices { implementation(location) implementation(integrity) @@ -311,6 +318,7 @@ dependencies { androidTestUtil(services) androidTestImplementation(navigation) androidTestImplementation(espresso) + androidTestImplementation(espressoIntents) } kotlinXTest { testImplementation(coroutinesTest) @@ -333,5 +341,5 @@ dependencies { } secrets { - defaultPropertiesFileName = "ci-overrides.properties" + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists()) "ci-overrides.properties" else "gradle.properties" } diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt index b2efda02..e03e94aa 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt @@ -84,6 +84,7 @@ 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.debug.data.Environment +import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AlertDialog @@ -441,6 +442,12 @@ fun DebugScreenMain( val modal = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() + val featuresState by produceState(initialValue = mutableMapOf()) { + viewModel.featuresState().collect { + value = it + } + } + ModalBottomSheetLayout( sheetContent = { EnvironmentSelector( @@ -463,6 +470,7 @@ fun DebugScreenMain( LaunchedEffect(Unit) { viewModel.state() } + val scope = rememberCoroutineScope() LazyColumn( state = listState, @@ -552,6 +560,25 @@ fun DebugScreenMain( item { FeatureToggles(viewModel = viewModel) } + + item { + DebugCard(title = "Login state") { + val profileHandler = LocalProfileHandler.current + val active = profileHandler.activeProfile + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = { + scope.launch { + profileHandler.switchProfileToPKV(active) + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Set User with ${active.name} as PKV", textAlign = TextAlign.Center) + } + } + } + } item { RotatingLog(viewModel = viewModel) } @@ -676,7 +703,9 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet ) Button( - modifier = Modifier.fillMaxWidth().testTag(TestTag.DebugMenu.SetVirtualHealthCardButton), + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.DebugMenu.SetVirtualHealthCardButton), onClick = { virtualHealthCardLoading = true scope.launch { @@ -754,10 +783,12 @@ fun EnvironmentSelector( Environment.values().forEach { Row( - modifier = Modifier.fillMaxWidth().clickable { - selectedEnvironment = it - onSelectEnvironment(it) - } + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedEnvironment = it + onSelectEnvironment(it) + } ) { Row( modifier = Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Small), diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 42ecd870..119a8d00 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:name=".App" android:foregroundServiceType="location"> + - + - 2022-01-06 Datenschutzerklaerung-E-Rezept-App - + 2021-07-02 Datenschutzerklaerung-E-Rezept-App + -

Datenschutzerklärung

+

Datenschutzerklärung

-

(Stand März 2022)

+

(Stand: Dezember 2022)

-

In dieser Datenschutzerklärung erfahren Sie, wie Ihre Daten bei Nutzung dieser App verarbeitet werden und welche Datenschutzrechte Sie haben. Die Datenschutzerklärung richtet sich an alle Nutzer dieser App.

+

Die E-Rezept-App ist die offizielle App zum E-Rezept in Deutschland. Die gematik gibt die App als Nationale Agentur für Digitale Medizin im Auftrag des Gesetzgebers heraus. Die gematik ist auch für den Datenschutz der App verantwortlich.

-

Im Rahmen der Nutzung dieser App werden bestimmte Ihrer personenbezogenen Daten verarbeitet. Personenbezogene Daten sind alle Informationen, die sich auf eine identifizierte oder identifizierbare natürliche Person beziehen. Verantwortlicher für die Verarbeitung dieser Daten im Rahmen der Nutzung der App ist die gematik GmbH. Ihre Daten werden nur so lange verarbeitet, wie es zur Erfüllung der festgelegten Zwecke und gesetzlicher Verpflichtungen erforderlich ist. Im Folgenden informieren wir Sie über Art, Umfang und Zweck der Verarbeitung der personenbezogenen Daten bei Nutzung der App.

+

Das offizielle Informationsangebot der gematik zum E-Rezept in Deutschland finden Sie unter www.das-e-rezept-fuer-deutschland.de. Dort erhalten Sie verständlich aufbereitete Informationen und Antworten auf häufig gestellte Fragen zum E-Rezept.

-

Inhalt

- -
- -

Wer ist für die Datenverarbeitung in der App verantwortlich?

- -

Betreiber der App ist die

- -

gematik GmbH ("gematik")
Friedrichstraße 136
10117 Berlin

- -

Die gematik ist auch dafür verantwortlich, dass Ihre personenbezogenen Daten durch die App in Übereinstimmung mit den Vorschriften über den Datenschutz verarbeitet werden. Bitte beachten Sie, dass die gematik nicht für die Verarbeitung Ihrer Daten außerhalb der App, z.B. in dem Fachdienst E-Rezept und dem Identitätsdienst in der Telematikinfrastruktur verantwortlich ist. Weitere Informationen zu der Rolle der weiteren Verantwortlichen finden Sie unter Ziffer 6 unserer Datenschutzerklärung.

- -

Bei Fragen zum Datenschutz oder zu Ihren Betroffenenrechten im Zusammenhang mit der Datenverarbeitung mittels dieser App können Sie sich jederzeit direkt an die gematik wenden. Den betrieblichen Datenschutzbeauftragten der gematik erreichen Sie über das Kontaktformular unter folgendem Link:

- -

https://www.gematik.de/hilfe-kontakt/kontaktformular/

- -

mit der Zuordnung des Anfragethemas „Datenschutz“.

- -

Um die App installieren zu können, müssen Sie ggf. zuvor bei einem Appstoreanbieter (z.B. Apple, Google) eine Nutzungsvereinbarung über den Zugang zu dem jeweiligen Appstore abschließen. Die gematik ist nicht Vertragspartner dieser Vereinbarung und hat keinen Einfluss auf die Datenverarbeitung bei dem Appstoreanbieter.

- -

Wie funktioniert das E-Rezept?

+

1. Über diese Datenschutzerklärung

-

Mit der E-Rezept-App können Sie E-Rezepte und ärztliche Verordnungen für Arzneimittel elektronisch empfangen, verwalten und bei Apotheken Ihrer Wahl einlösen.

+

In dieser Datenschutzerklärung informieren wir Sie über die Datenverarbeitung durch die E- Rezept-App. Wir bemühen uns um eine verständliche Darstellung der technischen Abläufe. Sollte uns das einmal nicht gelungen sein, lassen Sie es uns bitte wissen. Unsere Kontaktdaten finden Sie unter Ansprechpartner.

-

Wenn Sie z.B. zu einem Arzt gehen und dieser Ihnen ein Arzneimittel verschreibt wird dieser ein E-Rezept erstellen, das Sie über die E-Rezept-App abrufen können.

+

Diese Datenschutzerklärung gilt nicht für die Dienste von anderen Anbietern, die die zentralen Systeme des E-Rezepts bereitstellen. Zu diesen Diensten zählen der Rezeptdienst, der Identitätsdienst und das Apothekenverzeichnis. Informationen zu den verantwortlichen Anbietern dieser Dienste finden Sie unter Weitere Verantwortliche.

-

Der Arzt erzeugt das E-Rezept und übermittelt es direkt in die zentrale Telematikinfrastruktur. Er übermittelt das E-Rezept nicht direkt an Ihr Endgerät. Ein Zugriff auf die in dem Endgerät lokal gespeicherten Daten ist für den Arzt nicht möglich. Die Telematikinfrastruktur vernetzt alle Akteure des Gesundheitswesens und gewährleistet den sektoren- und systemübergreifenden sowie sicheren Austausch von Informationen. Dort befinden sich alle entscheidenden Funktionen für die Anwendungen des digitalen Gesundheitswesens gemäß Sozialgesetzbuch Fünftes Buch, Elftes Kapitel. Das E-Rezept ist eine eigenständige Anwendung der Telematikinfrastruktur.

+

2. Wozu dient diese App?

-

Sie können über die E-Rezept-App auf den Fachdienst E-Rezept in der Telematikinfrastruktur zugreifen und alle elektronischen Verordnungen, die Ihnen verschiedene Ärzte ausgestellt haben, über die App abrufen. Dies geschieht entweder durch den Scan eines 2D-Codes oder durch eine Authentifizierung mit der elektronischen Gesundheitskarte.

+

Wenn Sie von Ihrem Arzt oder Ihrer Ärztin ein elektronisches Rezept bekommen, wird es verschlüsselt im Rezeptdienst gespeichert. Der Rezeptdienst ist der zentrale Rezeptspeicher im deutschen Gesundheitsnetz. Mit der E-Rezept-App können Sie sicher auf den Rezeptdienst zugreifen, um E-Rezepte zu empfangen, zu verwalten und Apotheken zuzuweisen.

-

Mit der App können Sie nun Apotheken finden und bei diesen das E-Rezept einlösen. Das Einlösen kann durch das Vorzeigen des 2D-Codes in der Apotheke bei Abholung geschehen oder durch Übermittlung des 2D-Codes an die Apotheke mit anschließender Lieferung. Auch die Apotheke hat keinen Zugriff auf Ihre lokal gespeicherten Daten in der App. Erst wenn die Apotheke den 2D-Code gescannt hat, oder das E-Rezept erhält, kann die Apotheke die Daten direkt aus dem Fachdienst abrufen.

+

3. Ist die Nutzung der App freiwillig?

-

Welche Daten werden in der App zu welchen Zwecken verarbeitet?

+

Ja, die Nutzung der E-Rezept-App ist freiwillig. Sie sind auch nicht verpflichtet, bestimmte Daten in der App anzugeben oder sich anzumelden. Wenn Sie sich nicht anmelden, können Sie die App jedoch nur zum Scannen und Vorzeigen von Rezeptcodes in der Apotheke nutzen.

-

Bei der Nutzung der App erheben und verarbeiten wir in der App verschiedene Daten, die wir Ihnen nachfolgend entlang der Nutzungsweise der App erklären.

+

4. Wie und wozu werden Ihre Daten verarbeitet?

-

Was passiert, wenn ich die App öffne?

+

4.1 Profile einrichten

-

Die Erhebung Ihrer IP-Adresse durch unser System ist notwendig, um Ihnen die Nutzung unserer App zu ermöglichen. Bei dem Aufruf und während der Nutzung unserer App synchronisiert unser Server die Daten mit der App auf Ihrem Endgerät, damit Ihnen die aktuellen Informationen zu Ihren E-Rezepten zur Verfügung stehen. Dabei verarbeiten wird Ihre IP-Adresse, Ihr Internet-Service-Provider und das Datum und die Uhrzeit des Zugriffs.

-

Wir führen bei jedem Start der E-Rezept App eine Integritätsprüfung Ihres Gerätes durch. Smartphones können mit einem modifiziertem und somit potenziell unsicheren Betriebssystem ausgestattet werden. Nicht jeder Nutzer ist sich bewusst (z.B. bei gebraucht gekauften Geräten), dass sein Gerät „gerootet“ ist, und welche möglichen Gefahren damit einhergehen. Daher nutzen wir Google SafetyNet, um die Integrität des Gerätes zu prüfen, und informieren Nutzer, wenn ihr Gerät betroffen ist.

-

Um die Integrität zu prüfen, erhebt Google SafetyNet diverse Informationen über das Gerät und das installierte Betriebssystem, und leitet diese zur Integritätsprüfung an eigene Server. Diese Server befinden sich möglicherweise im außereuropäischen Ausland und unterliegen anderen Datenschutzgrundsätzen.

+

Sie müssen mindestens ein „Profil“ einrichten.

-

Was passiert, wenn Sie die Kamerafunktion nutzen / Rezepte mit der Kamera auslesen?

-

Wir verwenden ML Kit von Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland ("Google"), um den Rezept-QR-Code zu lesen. Der Rezeptcode ist eine eindeutige Identifizierung Ihres Rezeptes. Er kann verglichen werden mit der Nummer eines Schließfachs. Um diesen Code komfortabel und schnell auszulesen, wird das ML Kit genutzt. Die Verarbeitung des Rezeptcodes findet ausschließlich auf Ihrem Gerät statt. -

Bei dem ersten Starten des Rezeptcodescanners in unserer App, wird ML Kit auf Ihre Gerät heruntergeladen. Zu diesem Zweck erhebt Google Ihre IP-Adresse. Die Verarbeitung dient der Bereitstellung des Dienstes.

-

Darüber hinaus erhebt Google folgende nicht-personenbezogene Informationen zum Zwecke der Nutzungsanalyse, Diagnostik und Konfiguration des ML Kit:

    -
  • Geräteinformationen (z. B. Hersteller, Gerätemodell, Betriebssystemversion, Hardware, Mobilfunkbetreiber, Zeitzone und Spracheinstellungen)
  • -
  • Informationen über die Applikation (z.B. Version der App)
  • -
  • Informationen über die Konfiguration von ML Kit
  • -
  • Fehlermeldungen
  • -
  • Ereignistypen (initialisieren, Modell herunterladen, aktualisieren, ausführen, Erkennung)
  • -
  • Technische Leistungsdaten Ihres Gerätes
  • -
  • IP-Adresse (wird nur temporär gespeichert)
  • -
  • Weitere Daten, insbesondere Ihre Rezeptdaten, werden nicht von Google erhoben.
  • +
  • Die Daten zum Profil werden nur auf Ihrem Gerät gespeichert.
  • +
  • Der Profilname dient der übersichtlichen Anzeige. Er ist frei wählbar.
  • +
  • Sie können mehrere Profile einrichten, um E-Rezepte für Angehörige zu verwalten.
-

Die Verarbeitung Ihrer Informationen wird nicht nur von Google Ireland Limited, sondern kann auch von Google LLC in den USA durchgeführt werden. Weiterführendes unter 7. Übermittlung in Drittländer.

+

4.2 Anmelden am Rezeptdienst

-

Kann ich meine elektronische Gesundheitskarte verwenden?

+

Nur die Patientin/der Patient (bzw. eine Vertretungsperson), die verordnende Arztpraxis und die einlösende Apotheke dürfen auf die Daten im Rezeptdienst zugreifen dürfen. Für jeden Zugriff auf Ihre Daten im Rezeptdienst muss daher die Identität nachgewiesen werden, um die Zugriffsberechtigung zu überprüfen. Zuständig für diese Identitätsprüfung ist der Identitätsdienst. Informationen zum Anbieter dieses Dienstes finden Sie unter Weitere Verantwortliche.

-

Nach dem Starten der App können Sie sich mit Ihrer elektronischen Gesundheitskarte ("eGK") anmelden. Im Rahmen der Anmeldung erheben wir zunächst Ihre 6-stellige Zugangsnummer. Nach Eingabe Ihres PINs wird die eGK an Ihrem Endgerät eingelesen.

- -

Sie haben die Möglichkeit, dass die E-Rezept-App Ihre persönlichen Daten der eGK lokal auf Ihrem Endgerät speichert, damit Sie diese nicht bei jedem neuen Start der App neu eingeben müssen. Dazu gehören Namen, Krankenversicherungsnummer und die Zugangsnummer (Card Access Number, "CAN"). Die auf Ihrer eGK hinterlegten Zertifikate und weitergehenden Informationen werden nicht auf Ihre Endgeräte oder an uns übertragen.

- -

Wie kann ich meine E-Rezepte verwalten?

- -

Kern der E-Rezept-App ist die Anzeige von medizinischen Verordnungen bzw. E-Rezepten. Sie können die E-Rezepte nach der Anmeldung mit der eGK oder mittels Scan eines passenden 2D-Codes diesen verwalten.

- -

Die E-Rezept-App fragt Ihre persönlichen E-Rezepte nach Anmeldung mit der eGK aus dem Fachdienst ab, und speichert sie lokal auf Ihrem Endgerät. Sie können diese E-Rezepte jederzeit abrufen und in einer Apotheke einlösen, sowie diese E-Rezepte an Dritte wie z.B. Apotheken weiterleiten.

- -

Die E-Rezepte enthalten folgende Daten:

+

Sie können Ihre Identität mit Ihrer elektronischen Gesundheitskarte (eGK) und Ihrer ePA-App nachweisen:

    -
  • das verordnete Arzneimittel,
  • -
  • die Einnahmehinweise für das jeweilige Arzneimittel,
  • -
  • die Informationen über den Versicherten, insbesondere die Anschrift, das Geburtsdatum, die Krankenkasse, den Versichertenstatus und die Krankenversichertennummer,
  • -
  • die Informationen über den ausstellenden Arzt und ggf. die Praxis bzw. das Krankenhaus,
  • -
  • ggf. die Information ob es sich um einen Arbeitsunfall handelt,
  • -
  • die Dispensierinformationen, das heißt die Information über die tatsächlich ausgegebenen Arzneimittel, und
  • -
  • die Protokolldaten.
  • +
  • Wenn Sie sich der eGK anmelden, wird das digitale Authentifizierungszertifikat auf dem Chip kontaktlos (per NFC) ausgelesen und an den Identitätsdienst übertragen. Ihre Einwilligung erteilen Sie durch die Eingabe der PIN. Das Authentifizierungszertifikat enthält Ihre Versichertenstammdaten (z. B. Name und Ihre Versichertennummer) und gilt als elektronischer Identitätsnachweis. Sobald der Identitätsdienst das Authentifizierungszertifikat geprüft und Ihre Identität bestätigt hat, stellt er der E-Rezept-App einen Zugangsschlüssel (Token) aus. Mit diesem kann sich die E-Rezept-App am Rezeptdienst anmelden.
  • +
  • Wenn Sie sich mit der ePA-App anmelden, müssen Sie zunächst Ihre Krankenkasse auswählen, damit die E-Rezept-App sich mit der „richtigen“ ePA-App verbinden kann. Die Liste der Krankenkassen ruft die E-Rezept-App vom Identitätsdienst ab. Nachdem Sie Ihre Identitätsdaten über Ihre ePA-App mit der E-Rezept-App geteilt haben, leitet diese die Daten an den Identitätsdienst zur Prüfung weiter. Wenn Ihre Identität bestätigt werden kann, stellt der Identitätsdienst der E-Rezept-App den Zugangsschlüssel (Token) zur Anmeldung am Rezeptdienst aus.
-

Die Protokolle enthalten Informationen über die Zugriffe auf Ihre E-Rezepte. Unter anderem Informationen über den ausstellenden Arzt und die einlösende Apotheke.

+

Nach der Anmeldung werden Ihre im Rezeptdienst gespeicherten E-Rezepte und Abgabeinformationen heruntergeladen und zusätzlich auf Ihrem Gerät gespeichert.

-

Wie kann ich meine E-Rezepte einlösen?

+

Wenn Sie Ihre E-Rezepte über die E-Rezept-App verwalten, werden Ihre Aktivitäten an den Rezeptdienst übertragen und dort umgesetzt. Zu diesen Aktivitäten gehören beispielsweise die Vergabe von Zugriffsrechten und das Löschen von E-Rezepten.

-

Sie können die gespeicherte E-Rezepte in allen Apotheken einlösen. Dies können Sie entweder direkt in der Apotheke durch das Vorzeigen des 2D-Codes tun oder indem Sie den 2D-Code in der App an die Apotheke übermitteln.

+

4.3 Zugangsdaten speichern

-

Die Apotheke lädt anschließend das E-Rezept aus den Fachdienst herunter. Die Apotheke hat keinen direkten Zugriff auf die Daten aus der App auf Ihrem Endgerät. Nachdem das Arzneimittel herausgegeben worden ist, wird das E-Rezept durch die Apotheke in dem Fachdienst als herausgegeben markiert.

+

Wenn Sie angemeldet sind, können Sie für zukünftige Anmeldungen die Funktion „Zugangsdaten speichern“ aktivieren. In einem geschützten Bereich Ihres Geräts werden dann die Zugangsdaten – bestehend aus PIN, CAN, Authentifizierungszertifikat und Zugangsschlüssel (Token) – gespeichert, so dass sie erneut verwendet werden können. Diese Funktion kann nur aktiviert werden, wenn Ihr Smartphone über einen sicheren Mechanismus für den Zugriffsschutz von Apps verfügt. Diese Voraussetzung erfüllen aktuell Apple iPhones mit Touch ID oder Face ID und Android-Geräte mit „Strongbox Keymaster“-Sicherheitsmodul.

-

Damit Sie eine Apotheke in Ihrer Nähe leichter finden können, benutzt die App auch Ihre Standortdaten, wenn Sie dem zustimmen. Zur Durchführung der Suche nach einer Apotheke wird Ihr Standort von uns erhoben und an den Dienst „Apotheken-Verzeichnis“ übertragen und die Apotheken im Umkreis gesucht. Dabei wird Ihre Position durch diesen Dienst ausschließlich für diese Standort-Abfrage verwendet. Damit wir Ihnen die Nutzung des Dienstes ermöglichen können, verarbeiten wir auch hierfür Ihre IP-Adresse.

+

4.4 Rezeptcodes scannen

-

Was passiert mit den Kontaktdaten und der Lieferadresse?

-

Für manche Services ist die Angabe von Kontaktdaten erforderlich, um die Lieferung sicherzustellen oder um Sie in wichtigen Situationen zu kontaktieren, zum Beispiel, wenn Sie ein anderes Medikament erhalten, als der Arzt verschrieben hat, und sich die Einnahme verändert.

-

Die E-Rezept App verlangt die Eingabe von Kontaktdaten und Lieferadressen, wenn dies für die Erfüllung Ihres Belieferungswunsches erforderlich ist. Wenn diese Daten nicht erforderlich sind, haben Sie dennoch die Möglichkeit, freiwillig Kontaktdaten anzugeben, oder eine alternative Lieferadresse anzugeben. Ob die Eingabe erforderlich ist, oder nicht, wird Ihnen durch die E-Rezept App angezeigt und erklärt.

-

Die E-Rezept App übermittelt diese Daten an die beauftragte Apotheke. Der Datentransport erfolgt dabei verschlüsselt. Die gematik hat keine Kenntnis von den Klartextinhalten der Kommunikationsinhalte.

-

Kontaktdaten und Lieferadressen werden von der E-Rezept App für zukünftige Bestellungen gespeichert. Die Speicherung erfolgt nur auf Ihrem Endgerät. Die Daten werden verschlüsselt gespeichert.

+

Wenn Sie einen Rezeptcode speichern wollen, müssen Sie ihn mit der Kamera scannen. Beim Scannen wird eine elektronische Kopie des Rezeptcodes erzeugt. Dieser Arbeitsschritt findet ausschließlich auf Ihrem Gerät statt, das heißt es wird keine Internetverbindung benötigt.

-

Was passiert, wenn ich mich anmelde / mit einem Backend verbinde?

-

Wir führen bei jeder Kommunikation mit einem Backend des E-Rezept Systems eine Integritätsprüfung der App selbst durch. Es ist technisch möglich, eine veränderte Version der E-Rezept App herzustellen. Wir wollen Sie als individuellen Nutzer vor potenziellem Missbrauch durch eine gefälschte E-Rezept App zu schützen. Die Integritätsprüfung dient aber auch den Schutz unseres Rezeptdienstes, da so sichergestellt ist, dass er nur von berechtigten Anwendungen genutzt wird, und kein Missbrauch erfolgt. Sollten Sie eine fehlerhafte E-Rezept App installiert haben, so werden Sie informiert, und von der Kommunikation mit dem Rezeptdienst ausgeschlossen.

-

Für diese Integritätsprüfung nutzen wir Google SafetyNet. Um die Integrität zu prüfen, erhebt Google SafetyNet Informationen über das Gerät und das installierte Betriebssystem, und leitet diese zur Integritätsprüfung an eigene Server.

-

Die Verarbeitung Ihrer Informationen wird nicht nur von Google Ireland Limited, sondern kann auch von Google LLC in den USA durchgeführt werden. Weiterführendes unter 7. Übermittlung in Drittländer.

+

Hinweis für Android-Geräte: +Um Rezeptcodes auch unter ungünstigen Bedingungen (z. B. schlechte Kameraauflösung oder Lichtverhältnisse) fehlerfrei scannen zu können, nutzt die App mit Ihrer Zustimmung eine spezielle Schnittstelle (Barcode Scanning API von Google ML Kit). Diese Schnittstelle ist Bestandteil der Google Play Services, die auf Ihrem Gerät bereits installiert sind. Das Kamerabild wird dabei ausschließlich auf Ihrem Gerät analysiert. Wie bei allen Android-Geräten mit Google Play Services behält sich Google jedoch vor, bestimmte bei der Nutzung der Schnittstelle anfallende Nutzungs- und Gerätedaten (bei ML Kit z. B.: Hersteller, Gerätemodell, Betriebssystemversion, Hardware, Mobilfunkbetreiber, Zeitzone, Spracheinstellungen, IP-Adresse, App-Version, Konfiguration von ML Kit, Anzahl der Scanvorgänge, Fehlermeldungen) innerhalb des Betriebssystems zu protokollieren und zur Weiterentwicklung der Schnittstelle an einen Google-Server zu übermitteln. Dabei kann nicht ausgeschlossen werden, dass diese Daten in ein Drittland (z. B. USA) übermittelt werden, siehe An wen werden Ihre Daten weitergegeben. Sie können Ihre Zustimmung zu der Nutzung des Google ML Kit jederzeit widerrufen, indem Sie die Kamera-Berechtigung der E-Rezept-App in den Android-Einstellungen deaktivieren.

-

Kann ich über die App Nachrichten senden?

+

Weitere Informationen finden Sie in den Hinweisen von Google zu ML Kit und in der Google Datenschutzerklärung.

-

Die E-Rezept-App ermöglicht eine direkte Kommunikation zwischen Ihnen und Apotheken. Eine solche Kommunikation ist z.B. die Übermittlung von Kontaktdaten und der Lieferadresse.

-

Die Apotheke wiederum kann Ihnen Informationen zusenden, aber auch eine URL zustellen, die Sie aus der E-Rezept-App heraus öffnen können.

+

4.5 Apothekensuche

-

Die Kommunikation läuft über die Telematikinfrastruktur. Dazu ruft die E-Rezept-App die Kommunikationsdaten aus der Telematikinfrastruktur ab, und speichert diese lokal auf Ihrem Endgerät. Sie können diese Kommunikation in der E-Rezept-App jederzeit einsehen.

+

Bei Verwendung der Apothekensuche werden Ihre Suchkriterien (z. B. Adressen oder Standortdaten) an das Apotheken-Verzeichnis übermittelt.

-

Die Daten werden verschlüsselt übertragen. Die gematik hat keine Kenntnis von den Klartextinhalten der Kommunikationsinhalte.

+

4.6 Mitteilungen an Apotheken

-

Zur Übertragung und Darstellung der Kommunikationsinhalte verarbeitet die gematik die von Ihnen eingegebenen Nachrichten in den Freitextfeldern, ggf. die eingefügte Lieferadressen und Rufnummern, sowie die vollständigen Verordnungen, auf die sich die Nachrichten beziehen.

+

Die E-Rezept-App ermöglicht eine direkte Kommunikation zwischen Ihnen und Apotheken. Die Kommunikation läuft verschlüsselt und vertraulich über den Rezeptdienst, der die Mitteilungen auch archiviert. Weder der Anbieter des Rezeptdienstes noch die gematik haben Einblick in die ausgetauschten Mitteilungen. Die E-Rezept-App lädt die Mitteilungen vom Rezeptdienst und speichert sie auf Ihrem Gerät, so dass Sie auch ohne Anmeldung den aktuellen Stand sehen können.

-

Muss ich jedes Mal die Gesundheitskarte zur Anmeldung verwenden?

-

Nein. Bei bestimmten Telefonen können Sie die Anmeldung für einen längeren Zeitraum aufrechterhalten. Das funktioniert bei den meisten iPhone Modellen, sowie bei einigen Android Modellen. Grundlage ist das Vorhandensein eines Sicherheitsmerkmals, der s.g. „Strongbox“. Dies ist ein besonders sicherer Chip auf dem Telefon, der ein hohen Maß an Sicherheit garantiert.

-

Wenn Ihr Telefon die Anforderungen erfüllt, zeigt Ihnen die E-Rezept App an, dass Sie die „Zugangsdaten merken“ können.

-

Sie können diese Funktion auf mehreren Geräten ausführen. Damit Sie einen Überblick haben, welche Geräte alle Zugriff auf Ihre Rezeptdaten haben, zeigt Ihnen die E-Rezept App an, auf welchen Geräten Sie die Zugangsdaten gemerkt haben.

-

Wenn Sie die Zugangsdaten nicht mehr merken möchten, sondern wieder bei jeder Anmeldung die Gesundheitskarte nutzen möchten, können Sie dies in der E-Rezept App einstellen.

-

Wenn Sie anderen Geräten die Zugangsdaten entziehen möchten, können Sie dies auch in der E-Rezept App tun. Um diese Funktion ausführen zu können, müssen Sie sich möglicherweise mit der Gesundheitskarte authentifizieren.

-

Wenn Sie die Funktion „Zugangsdaten merken“ nutzen, dann wird auf Ihrem Smartphone ein Token verschlüsselt gespeichert. Dieses Token ist dem Authentifizierungsdienst des E-Rezept Systems (IDP - Identity-Provider) bekannt. Zusätzlich zu dem Token speichert der Authentifizierungsdienst auch die Gerätekennung und die Betriebssystemversion.

-

Wenn bestimmte Geräte oder Betriebssystemversionen als vulnerabel erkannt werden, werden betroffene Token vom Authentifizierungsdienst gelöscht. Sie werden durch die E-Rezept App dann aufgefordert, die eGK und PIN erneut zu nutzen.

+

4.7 Sicherheitsfunktionen

-

Wofür sind Profile da?

-

Mit Hilfe der Profile-Funktion können Sie z.B. für jedes Ihrer Familienmitglieder ein Profil anlegen, und diesem Profil die Gesundheitskarte des jeweiligen Familienmitglieds. Sie können dann mit der E-Rezept App für jedes Profil die betreffenden E-Rezepte laden und verwalten. Sie übernehmen damit die Rolle eines Bevollmächtigten.

-

Sie sind verantwortlich dafür, dass die Personen, deren Authentifizierungsmittel Sie hinterlegen, dieser Bevollmächtigung zugestimmt haben. Ferner sind Sie verantwortlich, auf Verlangen der Vollmacht-gebenden, die Vollmacht wieder aufzugeben. Sie können dies in der E-Rezept App durch Löschen des betroffenen Profiles durchführen.

-

Alle Profile und die hinterlegten Authentifizierungsmittel werden ausschließlich auf Ihrem Smartphone gespeichert.

+

(7.a) Verbundene Geräte

-

Wie kann (und darf) ich ein Profil anlegen, um Rezepte einer anderen Person zu erhalten?

-

Sie können jederzeit ein Profil anlegen, Die Funktion erreichen Sie über verschiedene Einstiegspunkte in der E-Rezept App. Sie können auch jederzeit Rezepte anderer Personen „fotografieren“ und in der E-Rezept App speichern.

-

Wenn Sie einem Profil ein Authentifizierungsmittel hinterlegen, z.B. die Gesundheitskarte, so muss die betroffene versicherte Person Sie dazu bevollmächtigt haben.

-

Alle Profile und die hinterlegten Authentifizierungsmittel werden ausschließlich auf Ihrem Smartphone gespeichert.

+

Die Funktion „Zugangsdaten speichern“ kann auf mehreren Geräten aktiviert werden. Jedes Gerät, auf dem Sie diese Funktion aktiviert ist, wird als „verbundenes Gerät“ bezeichnet. Alle verbundenen Geräte werden zentral auf dem Identitätsdienst verwaltet. Hierzu überträgt die E-Rezept-App Geräteinformationen zur eindeutigen Unterscheidbarkeit an den Identitätsdienst: Hersteller, Gerätemodell, Gerätename, Betriebssystemversion und Gerätename (z. B. „Annas iPhone“). Damit Sie den Überblick behalten, können Sie im Bereich „Einstellungen“ unter „Verbundene Geräte“ sehen, auf welchen Geräten Sie Ihre Zugangsdaten gespeichert haben.

-

Wenn ich jemandem erlaube, für mich Rezepte zu erhalten, was für Daten kann diese Person einsehen?

-

Eine bevollmächtigte Person kann alle Daten einsehen, die Sie auch einsehen könnten. Sie handelt an Ihrer statt. Der Rezeptdienst unterscheidet nicht, ob Sie oder die durch Sie bevollmächtigte Person Handlungen vornimmt.

+

Wenn Sie ein verbundenes Gerät abmelden möchten, können Sie dies auf dem betreffenden oder einem anderen verbundenen Gerät erledigen. Die auf dem abgemeldeten Gerät gespeicherten Zugangsdaten und die auf dem Identitätsdienst erfassten Geräteinformationen werden dann gelöscht.

-

Wie kann ich meine Erlaubnis, für mich Rezepte zu erhalten, wieder entziehen?

-

Wenn Sie eine andere Person bevollmächtigt haben, an Ihrer statt auf den Rezeptdienst zuzugreifen, und hierzu die Gesundheitskarte und PIN ausgehändigt haben, so müssen Sie zunächst die Gesundheitskarte zurückfordern, oder nötigenfalls durch Ihre Krankenkasse sperren lassen.

-

Die E-Rezept App ermöglicht es, die Zugangsdaten zum Rezeptdienst zu speichern. Um sicherzustellen, dass die ehemals bevollmächtigte Person keinen Zugang mehr hat, öffnen Sie bitte in der E-Rezept App die Übersicht aller Geräte, die „gemerkte Zugangsdaten“ haben. Sie finden diese Funktion im Menü. Sie können hier jedem Gerät die Zugangsdaten wieder entziehen. Und somit auch der ehemals bevollmächtigten Person, sofern diese auf ihrem Gerät die „Zugangsdaten gemerkt“ hat.

-

Die ehemals bevollmächtigte Person kann nach Entzug der Gesundheitskarte und der „Zugangsdaten merken“-Berechtigung nicht mehr auf Ihre Daten zugreifen, die auf dem Rezeptdienst liegen. Das heißt, die ehemals bevollmächtigte Person erhält keine neuen Rezepte, keine Statusänderung und keinen Zugriff auf Protokolldaten mehr.

-

Daten, die die ehemals bevollmächtigte Person vor Entzug der Bevollmächtigung einsehen konnte, bleiben auch weiterhin einsehbar, da diese Daten lokal auf dem Smartphone gespeichert werden.

+

Bitte beachten Sie: Wenn ein verbundenes Gerätemodell oder dessen Betriebsversion als unsicher eingestuft werden sollte, kann der Identitätsdienst das betreffende Gerät abmelden. Dabei werden die auf dem Identitätsdienst gespeicherten Geräteinformationen und der Zugangsschlüssel des Geräts gelöscht. Sie können sich auf dem abgemeldeten Gerät dann erneut am Rezeptdienst anmelden, sofern das Sicherheitsproblem durch ein Update des Betriebssystems beseitigt worden ist.

-

Kann ich mir Apotheken merken?

+

(7.b) Integritätsprüfungen

-

Sie können innerhalb der E-Rezept-App Apotheken favorisieren, so dass Sie schneller auf diese zugreifen können. Die Speicherung erfolgt nur auf Ihrem Endgerät. Die Daten werden verschlüsselt.

+

Smartphones können mit einem manipulierten Betriebssystem betrieben werden (sogenanntes „Jailbreaken“ oder „Rooten“). Nicht jeder betroffene Nutzer ist sich bewusst, dass sein Betriebssystem manipuliert worden ist (z. B. bei gebraucht gekauften Geräten) und welche Sicherheitsrisiken damit einhergehen. Bei jedem Start der E-Rezept-App wird daher eine technische Prüfung des Betriebssystems durchgeführt. Werden Hinweise auf eine Manipulation erkannt, wird eine Warnung angezeigt. Sie können die App anschließend auf eigenes Risiko ohne Einschränkungen weiternutzen.

-

Werden meine Daten analysiert?

- -

Es findet grundsätzlich keine Analyse Ihres Nutzungsverhaltens oder Ihrer Daten statt. Sie können jedoch zu der Weiterentwicklung der App beitragen, indem Sie in der App der Erhebung und Verarbeitung anonymer Nutzerdaten zustimmen. Ohne diese Zustimmung findet kein Analyse statt.

- -

Sofern Sie der Analyse zustimmen, verarbeiten wir die anonymisierten Nutzungsdaten, zum sicheren Betrieb und zur Weiterentwicklung der App. Die gematik hat keine Möglichkeiten, diese Daten Ihrer Person zuzuordnen und führt diese Daten auch nicht mit anderen Datenquellen zusammen. Im Rahmen der Nutzung dieser App werden Daten für statistische Zwecke in einer Protokolldatei gespeichert und an die für diesen Zweck beauftragter Piwik PRO GmbH übertragen.

- -

Wir erfassen dazu

- -
    -
  • Ihr Betriebssystem und
  • -
  • Handymodell,
  • -
  • die Verschlüsselungsfähigkeiten des Telefons,
  • -
  • den Zeitpunkt des Zugriffs und
  • -
  • die Art des Netzwerkes (Mobilfunk / Wifi)
  • -
+

Für die Jailbreak-Erkennung auf Apple-Geräten prüft die E-Rezept-App den Gerätespeicher auf Hinweise von Apps, die nur auf Geräten mit Jailbreak installiert werden können (z. B. Cydia-App).

-

von dem aus Sie zugreifen, die in der App aufgerufenen Screens (ohne den angezeigten Inhalt), die aktivierte Einstellungen in der App (App-Start abgesichert; Anmeldedaten gespeichert), die Anzahl fotografierter E-Rezepte, heruntergeladener E-Rezepte und eingelöster E-Rezepte, sowie die Zeitspanne zwischen Anfrage und Antwort von Servern des E-Rezept Systems.

-

Wir verarbeiten diese Daten, damit wir feststellen können, wann und welche Funktionen häufig benutzt werden, um die App besser gestalten zu können. Darüber hinaus dienen uns die Daten festzustellen, ob die eingesetzte Technik angepasst werden muss oder ob es bei den Nutzer Kompatibilitätsprobleme geben könnte.

-

Wir werden in keinem Fall Ihre Gesundheitsdaten nutzen, oder die Daten für werbliche Zwecke oder zur Identifikation von Personen verwenden. Die Daten werden nicht an Dritte wie z.B. Apotheken, Ärzte oder andere Einrichtungen weitergegeben.

+

Für die Root-Erkennung auf Android-Geräten wird eine Sicherheitsfunktion des Betriebssystems genutzt. Hierzu generiert die App eine Zufallszahl sowie Informationen zur E-Rezept-App (z. B. Version) und übergibt sie an eine spezielle Schnittstelle Ihres Betriebssystems (SafetyNet Attestation API). Diese Schnittstelle ist Bestandteil der Google Play Services, die auf Ihrem Gerät bereits installiert sind (siehe Rezeptcodes scannen). Google untersucht dann anhand der nur ihm zugänglichen Informationen zu Ihrem Gerät und der App, ob das Betriebssystem manipuliert wurde und teilt der E-Rezept-App das Prüfungsergebnis mit.

-

Welche Daten werden bei der Nutzung der Kartendienste in der App erhoben?

+

Weiterhin wird vor jeder Anmeldung am Rezeptdienst die Echtheit der App geprüft. Die Echtheitsprüfung dient dazu, festzustellen, ob Ihre Version der E-Rezept-App manipuliert oder gefälscht worden („unecht“) ist. Wenn Hinweise auf eine unechte App gefunden werden, kann sie sich nicht am Rezeptdienst anmelden. Sofern die E-Rezept-App auf einem verbundenen Gerät läuft, wird dieses getrennt und die unechte App abgemeldet. Für die Echtheitsprüfung nutzt die E-Rezept-App die Sicherheitsfunktionen Ihres Betriebssystems. Dies sind auf Apple-Geräten der Apple App Attest Service und auf Android-Geräten der SafetyNet App Attestation Service.

-

Wir binden die Kartendienste ein, um unsere App nutzerfreundlicher zu gestalten.

+

(7.c) App-Zugriffssperre

-

Wir binden in unserer App Kartenmaterial über die vom Betriebssystem Ihres Smartphones bereitgestellte API für Google Maps, Apple Maps und Huawei ein. Sie müssen der Nutzung der externen Kartendienste aktiv zustimmen werden. Nach Installation der App ist diese Funktion zunächst ausgeschaltet.

+

Um unbefugte Zugriffe auf Ihre E-Rezept-App zu erschweren, kann sie nach jedem Schließen automatisch gesperrt werden. Bei jedem Öffnen muss sie dann zunächst entsperrt werden. Je nachdem, über welche Sicherheitsausstattung Ihr Smartphone verfügt, stehen Kennwort- basierte und biometrische Verfahren zur Verfügung. +Bitte beachten Sie: Wenn Sie die Funktion „Zugangsdaten speichern“ aktivieren, wird automatisch auch die App-Sperre aktiviert.

-

Weitere Informationen zum Datenschutz im Zusammenhang mit Google, Apple und Huawei finden Sie hier:

+

Wenn Sie den Kennwortschutz aktivieren, wird Ihr Kennwort während der Einrichtung lokal auf ausreichende Stärke geprüft. Das Kennwort wird nur auf Ihrem Gerät gespeichert. +Bei Nutzung eines biometrischen Verfahrens werden die Sicherheitsfunktionen Ihres Smartphones verwendet. Die E-Rezept-App erhält dabei keine biometrischen Daten, sondern nur das Ergebnis der biometrischen Prüfung durch Ihr Betriebssystem.

- +

4.8 Push-Benachrichtigungen

-

Rechtsgrundlagen für die Datennutzung

+

Sie können sich per Push-Benachrichtigung über Mitteilungen von Apotheken oder neue E-Rezepte informieren lassen. Dazu registriert sich Ihr Smartphone bei dem Push-Dienst Ihres Betriebssystems. Folgende Push-Dienste werden genutzt: Firebase Cloud Messaging (Android), Apple Push Notification (Apple), Huawei Push Kit (Huawei).

-

Die E-Rezept-App verarbeitet Ihre personenbezogenen Daten zur Erfüllung der gesetzlichen Aufgabe der gematik, eine Komponente für den Zugriff von Versicherten auf elektronische Verordnungen bereit zu stellen. Deswegen verarbeiten wir die meisten Daten dieser App aufgrund einer rechtliche Verpflichtung und der Wahrnehmung einer Aufgabe die im öffentlichen Interesse liegt und die der gematik übertragen wurde, Art. 6 Abs. 1 S. 1 lit. c) und e), 9 Abs. 2 lit. h) DSGVO, §§ 311 Abs. 1 Nr. 10, 312 Abs. 4, 336 Abs. 1, 5 i.V.m. 334 Abs. 1 Nr. 6, 360 Abs. 10, 309 SGB V.

+

Der jeweilige Push-Dienst übergibt der E-Rezept-App eine Push-Kennung. Die Push-Kennung wird dann von der E-Rezept-App an den Rezeptdienst übermittelt. Soll eine Push-Nachricht an Sie versendet werden, schickt der Rezeptdienst einen Hinweis mit Ihrer Push-Kennung an den genutzten Push-Dienst, der diesen dann an Ihr Smartphone weiterleitet. Die Mitteilung der Apotheke bleibt dabei auf dem Rezeptdienst und wird erst geladen, wenn Sie die E-Rezept-App öffnen.

-

Soweit wir Ihre Standortdaten, die Daten zur Analyse oder die Daten zur Nutzung der Kartendienste verarbeiten geschieht dies aufgrund einer bei Ihnen vorab eingeholten Einwilligung. Sie können Ihre Einwilligung jederzeit und ohne Angaben von Gründen für die Zukunft widerrufen, ohne dass die Rechtmäßigkeit, der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung, berührt wird. Sie können durch die Änderung Ihrer Einstellungen in Ihrem Endgerät Ihre Einwilligung jederzeit widerrufen.

+

4.9 Nutzungsanalyse

-

Bitte beachten Sie, dass Sie durch die Nichterteilung der Einwilligung unter Umständen nicht in der Lage sind, die der angebotenen Services voll auszuschöpfen.

+

Wenn Sie der Nutzungsanalyse zustimmen, erfasst die gematik auf Basis Ihrer Einwilligung Informationen über Ihr Gerät und darüber, wie die App verwendet wird (z.B. Art, Version und Hersteller des Gerätes und des Betriebssystems, die Art und den Umfang der Nutzung und Bedienung der einzelnen App-Funktionen). Die Nutzungsdaten werden anonymisiert gespeichert. Ihre Einwilligung können Sie jederzeit widerrufen, indem Sie die Nutzungsanalyse in den Einstellungen deaktivieren. Ihre Nutzungsdaten werden dann nicht mehr an die gematik übertragen. Weitere Hinweise erhalten Sie vor dem Aktivieren der Nutzungsanalyse in der E-Rezept-App.

-

Aufbewahrung und Löschung der App-Daten

+

5. An wen werden Ihre Daten weitergegeben?

-

Die auf Ihrem Endgeräte lokal gespeicherten Daten werden nicht von uns verarbeitet.

+

Wenn Sie der Nutzungsanalyse zugestimmt haben, übermittelt die App Ihre Nutzungsdaten auf Basis Ihrer Einwilligung an den technischen Dienstleister der gematik für die Nutzungsanalyse (Content Square GmbH).

-

Sie können die Daten in der E-Rezept-App auf Ihrem Endgerät jederzeit selbst löschen, soweit das Betriebssystem Ihres Endgerätes dies zulässt. Wird das E-Rezept in der App gelöscht, löschen Sie zugleich auch die mit dem E-Rezept zusammenhängenden Daten auf Ihrem Endgerät.

+

Im Übrigen hat die gematik keinen Zugriff auf die auf Ihrem Gerät gespeicherten Daten. An andere Anbieter und sonstige Dritte gibt die App Ihre Daten nur im unter Wie und wozu werden Ihre Daten verarbeitet? beschriebenen Umfang weiter.

-

Sie können die Daten über die E-Rezept App auch auf dem Fachdienst löschen. Die E-Rezepte in dem Fachdienst werden nach gesetzlicher Vorgabe automatisch nach 100 Tagen ab dem Einstellen oder der letzten Änderung des jeweiligen E-Rezepts gelöscht.

+

Weder die E-Rezept-App noch die Anbieter des Rezeptdienstes, des Identitätsdienstes oder des Apothekenverzeichnisses übermitteln Ihre Daten in Länder, in denen kein angemessenes Datenschutzniveau besteht und europäische Datenschutzrechte eventuell nicht durchgesetzt werden (sogenannte Drittländer, z. B. USA).

-

Protokolleinträge können dagegen nicht durch Sie in der Telematikinfrastruktur gelöscht werden. Sie werden entsprechend der gesetzlichen Vorgaben automatisch 3 Jahre nach Erzeugung von den Servern gelöscht.

+

Wir weisen Sie jedoch darauf hin, dass durch die Nutzung der E-Rezept-App auf Seiten Ihres Betriebssystems Nutzungsdaten anfallen. Die Hersteller der Betriebssysteme behalten sich teilweise vor, diese Daten zu protokollieren und auszuwerten. Dabei kann es auch zu einer Übermittlung von Geräte- und Nutzungsdaten in die USA oder ein anderes Drittland kommen. Insoweit besteht die Möglichkeit, dass Sicherheitsbehörden im Drittland auf die übermittelten Daten beim Hersteller zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen über Sie verknüpfen.

-

Bitte beachten Sie, dass nicht die gematik, sondern IBM verantwortliche Stelle für die Verarbeitung Ihrer Daten in dem Fachdienst E-Rezept in der Telematikinfrastruktur ist. Weitere Informationen zu der Rolle von IBM finden Sie unter Ziffer 6 a) unserer Datenschutzerklärung.

+

6. Wann werden Ihre Daten gelöscht?

-

Die Standortdaten im Rahmen der Abfrage im Apotheken-Verzeichnis werden nur im Rahmen der Abfrage-Session gespeichert und danach sofort gelöscht.

+

Die gematik speichert keine personenbezogenen Daten von Nutzern der E-Rezept-App. Seitens der gematik ist daher keine Löschung notwendig oder möglich.

-

Weitergabe von Daten an Dritte

- -

Ihre Daten werden streng vertraulich behandelt. Soweit dies nicht in dieser Datenschutzerklärung ausdrücklich vorgesehen ist, werden Ihre Daten nicht an Dritte weitergeben, es sei denn, Sie haben in die Weitergabe ausdrücklich eingewilligt. Technische Dienstleister unterstützen die gematik dabei, die Funktionalitäten des E-Rezeptes bereitzustellen. Die Dienstleister sind verpflichtet, sämtliche notwendigen technischen und organisatorischen Maßnahmen zu ergreifen, um Ihre Daten gemäß den datenschutzrechtlichen Erfordernissen zu schützen. Eine Weitergabe an Dritte oder Verwendung für andere Zwecke ist ihnen nicht gestattet.

+

Die auf Ihrem Gerät gespeicherten Rezeptdaten, Rezeptcodes und Mitteilungen können Sie in der App selbst löschen. Bitte beachten Sie, dass gelöschte E-Rezepte erneut auf Ihrem Gerät gespeichert werden, sobald Sie die Rezeptansicht aktualisieren. Dies können Sie verhindern, indem Sie die E-Rezepte auch im Rezeptdienst löschen. Die E-Rezepte im Rezeptdienst werden automatisch nach 100 Tagen ab Ausstellung oder letzter Statusänderung gelöscht (§ 360 Abs. 11 SGB V).

-

Beteiligte Verantwortliche gemäß § 307 Abs. 4 SGB V und deren Zwecke:

- -

E-Rezept Fachdienst

- - ---- - - - - - - - - - - - - - - - - - - -
NameIBM Deutschland GmbH
AnschriftIBM-Allee 1
71139 Ehningen
AufgabenfeldEntwicklung, Bereitstellung und Betrieb eines Fachdienstes E-Rezept zur Bereitstellung der Funktionen von E-Rezepten.
Der Fachdienst ist ein Server auf dem die E-Rezepte gespeichert werden. Berechtigte Ärzte, Versicherte und Apotheker können auf den Server zugreifen. Er garantiert, dass ein E-Rezept echt ist und dass es nur ein einziges Mal eingelöst werden kann.
- -

Identitätsdienst

- - ---- - - - - - - - - - - - - - - - - - - -
NameResearch Industrial Systems Engineering (RISE) Forschungs-, Entwicklungs- und Großprojektberatung GmbH
AnschriftConcorde Business Park F
2320 Schwechat
Austria
AufgabenfeldEntwicklung, Bereitstellung und Betrieb eines Dienstes Identity Provider (IDP) der den berechtigten Zugang von Nutzern an den E-Rezept Fachdienst durch Bereitstellung von Authentifizierungs-Mitteln unterstützt.
Der IDP ist ein Server. Der IDP erstellt ein mathematisches Rätsel, dass nur von einer Gesundheitskarte richtig beantworten werden kann. Wird das Rätsel richtig beantwortet, stellt der IDP einen "Schlüssel" aus, mit dem auf den Fachdienst zugegriffen werden kann. Dieser Schlüssel ist nur für eine Gesundheitskarte gültig. Dadurch kann nur auf E-Rezepte dieser Gesundheitskarte zugegriffen werden.
- -

Beteiligte Auftragsverarbeiter und deren Zwecke:

- -

Statistische Auswertung

- - ---- - - - - - - - - - - - - - - - - - - -
NamePiwik PRO GmbH
AnschriftKurfürstendamm 21
10719 Berlin
AufgabenfeldBereitstellung und Betrieb der Piwik PRO Analytics Suite.
Dient der Bereitstellung von statistische Auswertung anonymisierter Nutzungsdaten, um den sicheren Betrieb und die Weiterentwicklung der App zu unterstützen.
- -

Apothekenverzeichnisdienst

- - ---- - - - - - - - - - - - - - - - - - - -
NameDeutschen Apothekerverband e. V.
AnschriftHeidestraße 7
10557 Berlin
AufgabenfeldKonzeption, den Aufbau und den Betrieb eines Apotheken-Verzeichnisses zur Bereitstellung von Funktionen für die standortbasierte Suche von Apotheken im Rahmen der Einlösung von E-Rezepten.
Vergleiche auch Standortbezogenen Daten
- -

Übermittlung in Drittländer

- -

Google Dienste ML KIT und SafetyNet

-

Durch die Verwendung von google Diensten werden Informationen wird nicht nur von Google Ireland Limited, sondern können auch von Google LLC in den USA durchgeführt werden.

-

Die USA ist ein Drittland, dass nicht von einem Angemessenheitsbeschluss der Europäischen Kommission erfasst wird, und daher kein angemessenes Schutzniveau für personenbezogene Daten bietet.

-

Google stützt sich für den internationalen Datentransfer auf die Standarddatenschutzklauseln. Eine Kopie der Standarddatenschutzklauseln können Sie unter - https://policies.google.com/privacy/frameworks und - https://support.google.com/policies/contact/general_privacy_form erhalten.

-

Kartendienste

-

Bei der Verwendung der Kartendienste kann es zu einer Übermittlung in die USA kommen. Die USA ist ein Drittland, dass nicht von einem Angemessenheitsbeschluss der Europäischen Kommission erfasst wird, und daher kein angemessenes Schutzniveau für personenbezogene Daten bietet. Für die Übertragung in die USA stützt sich Google, Apple und Huawei auf Standardvertragsklauseln. Mehr Informationen findest du unter

- +

Zugriffsprotokolle im Rezeptdienst werden nicht auf Ihrem Gerät gespeichert und können nicht gelöscht werden. Sie werden nach drei Jahren automatisch im Rezeptdienst gelöscht (§ 309 SGB V).

-

Gibt es eine automatisierte Entscheidungsfindung einschließlich Profiling?

+

Wenn Sie ein Profil löschen, werden alle damit zusammenhängenden Daten einschließlich der vom Rezeptdienst heruntergeladenen Daten und der gespeicherten Zugangsdaten gelöscht. Die gespeicherten Zugangsdaten werden auch gelöscht, wenn Sie „Zugangsdaten speichern“ deaktivieren.

-

Es wird keine vollautomatisierte Entscheidungsfindung einschließlich Profiling gemäß Artikel 22 DS-GVO genutzt.

+

Wenn Sie Push-Benachrichtigungen in der E-Rezept-App deaktivieren, wird die auf Ihrem Gerät gespeicherte Push-Kennung gelöscht.

-

Maßnahmen zur Sicherheit der Daten

+

Die Deinstallation der E-Rezept-App bewirkt die Löschung sämtlicher von der E-Rezept-App auf Ihrem Gerät gespeicherten Daten.

-

Die in der Telematikinfrastruktur und auf der App enthaltenen Daten können sensible Daten enthalten, die Rückschlüsse auf Ihre Gesundheit und Ihre Leben ermöglichen. Aus diesem Grund verwenden wir sichere Technologien, um Ihre Daten bestmöglich zu schützen.

+

7. Ihre Datenschutzrechte

-

Was wir tun

+

Bezüglich der mit Ihrer Einwilligung an die gematik übermittelten Nutzungsdaten stehen Ihnen gegenüber der gematik die Rechte auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO) sowie Datenübertragbarkeit (Art. 20 DSGVO) zu. Sie haben auch das Recht, Ihre Einwilligung zu widerrufen (siehe hierzu unter Nutzungsanalyse). Bitte beachten Sie, dass die gematik Ihre Nutzungsdaten in anonymisierter Form speichert. Eine Zuordnung dieser Daten zu Ihrer Person ist nicht mehr möglich, so dass die oben genannten Datenschutzrechte keine Anwendung mehr finden (Art. 11 Abs. 2 DSGVO, § 308 SGB V).

-

Die von einem Arzt ausgestellten E-Rezepte werden verschlüsselt zum E-Rezept-Fachdienst übertragen, dort verschlüsselt gespeichert und verarbeitet und wieder verschlüsselt vom Apotheker abgerufen und damit vor Unbefugten – auch dem Betreiber des E-Rezept-Fachdienstes -geschützt.

+

Sie haben außerdem das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren. Die für die gematik zuständige Datenschutz-Aufsichtsbehörde ist der Bundesbeauftragte für den Datenschutz und die Informationsfreiheit.

-

Es können nur Personen ein E-Rezept vom E-Rezept-Fachdienst abrufen, wenn sie im Besitz des 2D-Code oder der darin enthalten Zugriffsinformationen sind, die der Patient entweder von seinem Arzt auf Papier erhalten hat oder durch die Nutzung der E-Rezept-App elektronisch erzeugt hat. Nur wenn der Patient seinen 2D-Code an einen Vertreter oder einen Apotheker weitergibt, können diese Personen auf das zugehörige E-Rezept zugreifen.

+

8. Weitere Verantwortliche

-

Sowohl die E-Rezept-App als auch der E-Rezept-Fachdienst wurden von unabhängigen Gutachtern geprüft. Die gematik überwacht die Einhaltung von Datenschutz und Sicherheit durch technische Systeme und durch regelmäßige Audits beim Betreiber des E-Rezept-Fachdienstes.

+

Für die folgenden Dienste sind gemäß § 307 Abs. 4 SGB V die jeweiligen Anbieter datenschutzrechtlich verantwortlich:

-

Was wir Ihnen ermöglichen

+
    +
  • Rezeptdienst: IBM Deutschland GmbH, IBM-Allee 1, 71139 Ehningen
  • +
  • Identitätsdienst: Research Industrial Systems Engineering (RISE) Forschungs-, Entwicklungs- und Großprojektberatung GmbH, Concorde Business Park F, 2320 Schwechat, Österreich
  • +
  • Apothekenverzeichnis: Deutscher Apothekerverband e.V., Heidestraße 7, 10557 Berlin
  • +
-

Um die Sicherheit weiter zu erhöhen, haben Sie innerhalb der E-Rezept App die Möglichkeit ein Kennwort festzulegen. Der Klartext des Kennwortes wird zu keiner Zeit gespeichert. Darüber hinaus haben Sie auch die Möglichkeit, soweit dies das jeweilige Endgerät zulässt, sich über die Touch- und Face-ID anzumelden.

+

9. Ansprechpartner

-

Durch diese Maßnahmen können Sie Ihre Daten noch stärker schützen. Insbesondere bei einem Verlust Ihres Endgerätes kann die Gefahr vor dem Missbrauch Ihrer Daten stark verringert werden.

+

Die Kontaktdaten der gematik können Sie dem Impressum entnehmen. Weitere Kontaktmöglichkeiten finden Sie in den Einstellungen unter „Kontakt“.

-

Betroffenenrechte

+

Bei Fragen zum Datenschutz oder zu Ihren Datenschutzrechten im Zusammenhang mit dieser App können Sie sich an den Datenschutzbeauftragten der gematik wenden.

-

Gemäß den gesetzlichen Bestimmungen zum Datenschutz haben Sie jederzeit das Recht:

+

Sie erreichen ihn

    -
  • Auskunft über Ihre verarbeiteten Daten sowie eine Kopie dieser Daten zu verlangen (Recht auf Auskunft);
  • -
  • die Berichtigung unrichtiger Daten und, unter Berücksichtigung der Zwecke der Verarbeitung, die Vervollständigung unvollständiger Daten zu verlangen (Recht auf Berichtigung);
  • -
  • bei Vorliegen berechtigter Gründe die Löschung Ihrer Daten zu verlangen (Recht auf Löschung);
  • -
  • die Einschränkung der Verarbeitung Ihrer Daten zu verlangen, sofern die gesetzlichen Voraussetzungen gegeben sind) (Recht auf Einschränkung der Verarbeitung);
  • -
  • nicht einer ausschließlich auf einer automatisierten Verarbeitung beruhenden Entscheidung unterworfen zu sein, sofern die gesetzlichen Voraussetzungen hierfür nicht vorliegen; eine automatisierte Entscheidungsfindung wird von der gematik nicht durchgeführt.
  • -
  • Sie haben ferner das Recht, der Verarbeitung Ihrer Daten, aus Gründen, die sich aus Ihrer besonderen Situation ergeben, nach Maßgabe der gesetzlichen Bestimmungen zu widersprechen (Widerspruchsrecht).
  • -
  • (Recht auf Datenübertragbarkeit)
  • +
  • über das Kontaktformular: https://www.gematik.de/hilfe-kontakt/kontaktformular/ mit der Zuordnung des Anfragethemas „Datenschutz“
  • +
  • per Post: gematik GmbH, Datenschutzbeauftragter, Friedrichstraße 136, 10117 Berlin
  • +
  • per E-Mail: datenschutz@gematik.de
-

Für die Ausübung Ihrer Rechte kontaktieren Sie uns bitte über das Kontaktformular unter folgendem Link: -https://www.gematik.de/hilfe-kontakt/kontaktformular/ – -mit der Zuordnung des Anfragethemas „Datenschutz“.

- -

Wir sind nicht dazu verpflichtet Ihre Anfrage zu beantworten, wenn dies nur unter Aufhebung der Verschlüsselung der Daten möglich ist.

- -

Darüber hinaus besteht für Sie das Recht der Beschwerde beim

- -

Bundesbeauftragten für Datenschutz und die Informationsfreiheit (BfDI)
Graurheindorfer Str. 15
53117 Bonn
poststelle@bfdi.bund.de oder poststelle@bfdi.de-mail.de

+

Bei Fragen zum Gesundheitsnetz (Telematikinfrastruktur) und insbesondere zu den datenschutzrechtlichen Verantwortlichkeiten der beteiligten Anbieter können Sie sich an den Datenschutzlotsen der gematik wenden: https://www.gematik.de/datensicherheit/datenschutzlotse

diff --git a/android/src/main/java/de/gematik/ti/erp/app/App.kt b/android/src/main/java/de/gematik/ti/erp/app/App.kt index 049b8fc0..36a97cb7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/App.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/App.kt @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app import android.app.Application import android.content.Context import androidx.lifecycle.ProcessLifecycleOwner +import com.contentsquare.android.Contentsquare import de.gematik.ti.erp.app.core.AppScopedCache import de.gematik.ti.erp.app.di.allModules import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase @@ -56,6 +57,8 @@ class App : Application(), DIAware { ProcessLifecycleOwner.get().lifecycle.apply { addObserver(authUseCase) } + + Contentsquare.start(this) } companion object { diff --git a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt b/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt index 17381836..c27b6f6f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt @@ -62,7 +62,7 @@ import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPrompt import de.gematik.ti.erp.app.cardwall.mini.ui.MiniCardWallViewModel import de.gematik.ti.erp.app.cardwall.mini.ui.rememberAuthenticator import de.gematik.ti.erp.app.cardwall.ui.CardWallNfcPositionViewModel -import de.gematik.ti.erp.app.cardwall.ui.CardWallViewModel +import de.gematik.ti.erp.app.cardwall.ui.CardWallController import de.gematik.ti.erp.app.cardwall.ui.ExternalAuthenticatorListViewModel import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.core.LocalAuthenticator @@ -122,7 +122,7 @@ class MainActivity : AppCompatActivity(), DIAware { bindProvider { UnlockEgkViewModel(instance(), instance()) } bindProvider { MiniCardWallViewModel(instance(), instance(), instance(), instance(), instance()) } bindProvider { CardWallNfcPositionViewModel(instance()) } - bindProvider { CardWallViewModel(instance(), instance(), instance()) } + bindProvider { CardWallController(instance(), instance(), instance()) } bindProvider { ExternalAuthenticatorListViewModel(instance(), instance()) } bindProvider { HealthCardOrderViewModel(instance()) } bindProvider { PrescriptionDetailsViewModel(instance(), instance()) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt b/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt index ca607b63..a7520ec2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt @@ -88,6 +88,17 @@ object TestTag { val FullDetailPrescription by tagName() val FullDetailPrescriptionName by tagName() + val PrescriptionRedeemable by tagName() + val PrescriptionWaitForResponse by tagName() + val PrescriptionInProgress by tagName() + val PrescriptionRedeemed by tagName() + + val ArchiveButton by tagName() + + object Archive { + val Content by tagName() + } + object Details { val Content by tagName() val Screen by tagName() @@ -205,6 +216,17 @@ object TestTag { val Screen by tagName() val PrescriptionListItem by tagName() + val MessageListItem by tagName() + } + + object Messages { + val Content by tagName() + val Link by tagName() + val LinkButton by tagName() + val Text by tagName() + val Code by tagName() + val CodeLabelContent by tagName() + val Empty by tagName() } } 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 703863b0..f2e8e998 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 @@ -19,73 +19,84 @@ package de.gematik.ti.erp.app.analytics import android.content.Context +import android.content.SharedPreferences import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.core.content.edit import androidx.navigation.NavHostController +import com.contentsquare.android.Contentsquare +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.core.LocalAnalytics import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import io.github.aakira.napier.Napier import java.security.MessageDigest -private const val TrackerName = "Tracker" +private const val PrefsName = "analyticsAllowed" // `gemSpec_eRp_FdV A_20187` class Analytics constructor( - private val context: Context + private val context: Context, + private val prefs: SharedPreferences ) { - private val _trackingAllowed = MutableStateFlow(false) - val trackingAllowed: StateFlow - get() = _trackingAllowed + private val _analyticsAllowed = MutableStateFlow(false) + val analyticsAllowed: StateFlow + get() = _analyticsAllowed - private val prefsName = "pro.piwik.sdk_" + - MessageDigest.getInstance("MD5").digest(TrackerName.toByteArray()) + // TODO remove in future versions + private val piwikPrefsName = "pro.piwik.sdk_" + + MessageDigest.getInstance("MD5").digest("Tracker".toByteArray()) .joinToString(separator = "") { eachByte -> "%02X".format(eachByte) } init { - Napier.d("Init tracker") + Napier.d("Init Analytics") - _trackingAllowed.value = !context.getSharedPreferences( - prefsName, + Contentsquare.forgetMe() + + // TODO remove in future versions + val piwikOptOut = !context.getSharedPreferences( + piwikPrefsName, Context.MODE_PRIVATE ).getBoolean("tracker.optout", true) + + _analyticsAllowed.value = prefs.getBoolean(PrefsName, !piwikOptOut) + if (_analyticsAllowed.value) { + allowTracking() + } else { + disallowTracking() + } + } + + fun tagScreen(screenName: String) { + if (analyticsAllowed.value) { + Contentsquare.send(screenName) + Napier.d("Analytics send $screenName") + } } fun allowTracking() { - _trackingAllowed.value = true + _analyticsAllowed.value = true - context.getSharedPreferences( - prefsName, - Context.MODE_PRIVATE - ).let { prefs -> - prefs.edit { - putBoolean("tracker.optout", false) - } + Contentsquare.optIn(context) + + prefs.edit { + putBoolean(PrefsName, true) } - Napier.d("Tracking allowed") + Napier.d("Analytics allowed") } fun disallowTracking() { - _trackingAllowed.value = false + _analyticsAllowed.value = false - context.getSharedPreferences( - prefsName, - Context.MODE_PRIVATE - ).let { prefs -> - prefs.edit { - putBoolean("tracker.optout", true) - } + Contentsquare.optOut(context) + + prefs.edit { + putBoolean(PrefsName, false) } - Napier.d("Tracking disallowed") - } - - @Suppress("UnusedPrivateMember") - fun trackScreen(path: String) { - // noop + Napier.d("Analytics disallowed") } fun trackIdentifiedWithIDP() { @@ -116,15 +127,41 @@ class Analytics constructor( @Composable fun TrackNavigationChanges(navController: NavHostController) { - val tracker = LocalAnalytics.current + val analytics = LocalAnalytics.current LaunchedEffect(Unit) { navController.currentBackStackEntryFlow.collect { try { - tracker.trackScreen(Uri.parse(it.destination.route).buildUpon().clearQuery().build().toString()) + analytics.tagScreen(Uri.parse(it.destination.route).buildUpon().clearQuery().build().toString()) } catch (expected: Exception) { Napier.e("Couldn't track navigation screen", expected) } } } } +fun Analytics.trackAuth(state: AuthenticationState) { + if (analyticsAllowed.value) { + when (state) { + AuthenticationState.HealthCardBlocked -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.CardBlocked) + AuthenticationState.HealthCardCardAccessNumberWrong -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.CardAccessNumberWrong) + AuthenticationState.HealthCardCommunicationInterrupted -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.CardCommunicationInterrupted) + AuthenticationState.HealthCardPin1RetryLeft, + AuthenticationState.HealthCardPin2RetriesLeft -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.CardPinWrong) + AuthenticationState.IDPCommunicationFailed -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.IDPCommunicationFailed) + AuthenticationState.IDPCommunicationInvalidCertificate -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.IDPCommunicationInvalidCertificate) + AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.IDPCommunicationInvalidOCSPOfCard) + AuthenticationState.SecureElementCryptographyFailed -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.SecureElementCryptographyFailed) + AuthenticationState.UserNotAuthenticated -> + trackAuthenticationProblem(Analytics.AuthenticationProblem.UserNotAuthenticated) + else -> {} + } + } +} 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 68bc274b..26816fb8 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 @@ -27,8 +27,5 @@ object UnlockEgkNavigation { object OldSecret : Route("OldSecret") object NewSecret : Route("NewSecret") object UnlockEgk : Route("UnlockEgk") - object TroubleshootingPageA : Route("TroubleshootingPageA") - object TroubleshootingPageB : Route("TroubleshootingPageB") - object TroubleshootingPageC : Route("TroubleshootingPageC") - object TroubleshootingNoSuccessPage : Route("TroubleshootingNoSuccessPage") + object TroubleShooting : Route("TroubleShooting") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEGKTroubleshooting.kt b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEGKTroubleshooting.kt deleted file mode 100644 index 28e289ad..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEGKTroubleshooting.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.cardunlock.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardwall.ui.TroubleshootingNoSuccessPageContent -import de.gematik.ti.erp.app.cardwall.ui.TroubleshootingPageAContent -import de.gematik.ti.erp.app.cardwall.ui.TroubleshootingPageBContent -import de.gematik.ti.erp.app.cardwall.ui.TroubleshootingPageCContent -import kotlinx.coroutines.launch - -@Suppress("LongParameterList") -@Composable -fun UnlockEGKTroubleshootingPageA( - viewModel: UnlockEgkViewModel, - cardAccessNumber: String, - personalUnblockingKey: String, - unlockMethod: UnlockMethod, - oldSecret: String, - newSecret: String, - onRetryOldSecret: () -> Unit, - onRetryCan: () -> Unit, - onRetryPuk: () -> Unit, - onFinishUnlock: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onAssignPin: () -> Unit -) { - val dialogState = rememberUnlockEgkDialogState() - UnlockEgkDialog( - dialogState = dialogState, - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryOldSecret = onRetryOldSecret, - onRetryCan = onRetryCan, - onRetryPuk = onRetryPuk, - onFinishUnlock = onFinishUnlock, - onAssignPin = onAssignPin - ) - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageAContent( - onBack = onBack, - onNext = onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Suppress("LongParameterList") -@Composable -fun UnlockEGKTroubleshootingPageB( - viewModel: UnlockEgkViewModel, - unlockMethod: UnlockMethod, - cardAccessNumber: String, - personalUnblockingKey: String, - oldSecret: String, - newSecret: String, - onRetryCan: () -> Unit, - onRetryOldSecret: () -> Unit, - onRetryPuk: () -> Unit, - onFinishUnlock: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onAssignPin: () -> Unit -) { - val dialogState = rememberUnlockEgkDialogState() - UnlockEgkDialog( - dialogState = dialogState, - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryCan = onRetryCan, - onRetryOldSecret = onRetryOldSecret, - onRetryPuk = onRetryPuk, - onFinishUnlock = onFinishUnlock, - onAssignPin = onAssignPin - ) - - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageBContent( - onBack, - onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Suppress("LongParameterList") -@Composable -fun UnlockEGKTroubleshootingPageC( - viewModel: UnlockEgkViewModel, - unlockMethod: UnlockMethod, - cardAccessNumber: String, - personalUnblockingKey: String, - oldSecret: String, - newSecret: String, - onRetryCan: () -> Unit, - onRetryOldSecret: () -> Unit, - onRetryPuk: () -> Unit, - onFinishUnlock: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onAssignPin: () -> Unit -) { - val dialogState = rememberUnlockEgkDialogState() - UnlockEgkDialog( - dialogState = dialogState, - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryCan = onRetryCan, - onRetryOldSecret = onRetryOldSecret, - onRetryPuk = onRetryPuk, - onFinishUnlock = onFinishUnlock, - onAssignPin = onAssignPin - ) - - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageCContent( - onBack, - onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Composable -fun UnlockEGKTroubleshootingNoSuccessPage( - onNext: () -> Unit, - onBack: () -> Unit -) { - TroubleshootingNoSuccessPageContent( - onNext, - onBack - ) -} 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 dfc71586..80ade9b1 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 @@ -40,7 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -53,6 +53,7 @@ import de.gematik.ti.erp.app.cardwall.ui.CardWallNfcPositionViewModel import de.gematik.ti.erp.app.cardwall.ui.ConformationSecretInputField import de.gematik.ti.erp.app.cardwall.ui.NFCInstructionScreen import de.gematik.ti.erp.app.cardwall.ui.SecretInputField +import de.gematik.ti.erp.app.troubleShooting.TroubleShootingScreen import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -78,11 +79,10 @@ sealed class ToggleUnlock { data class ToggleByHealthCard(val tag: Tag) : ToggleUnlock() } -@Suppress("LongMethod") @Composable fun UnlockEgKScreen( unlockMethod: UnlockMethod, - navController: NavController, + onCancel: () -> Unit, onClickLearnMore: () -> Unit ) { val viewModel by rememberViewModel() @@ -94,23 +94,9 @@ fun UnlockEgKScreen( var oldSecret by rememberSaveable { mutableStateOf("") } var newSecret by rememberSaveable { mutableStateOf("") } - val onRetryCan = { - unlockNavController.navigate(UnlockEgkNavigation.CardAccessNumber.path()) { - popUpTo(UnlockEgkNavigation.CardAccessNumber.path()) { inclusive = true } - } - } - - val onRetryOldSecret = { - unlockNavController.navigate(UnlockEgkNavigation.OldSecret.path()) { - popUpTo(UnlockEgkNavigation.OldSecret.path()) { inclusive = true } - } - } - - val onRetryPuk = { - unlockNavController.navigate(UnlockEgkNavigation.PersonalUnblockingKey.path()) { - popUpTo(UnlockEgkNavigation.PersonalUnblockingKey.path()) { inclusive = true } - } - } + val onRetryCan = onRetryCan(unlockNavController) + val onRetryOldSecret = onRetryOldSecret(unlockNavController) + val onRetryPuk = onRetryPuk(unlockNavController) val onAssignPin = { unlockMethod = UnlockMethod.ChangeReferenceData @@ -119,9 +105,6 @@ fun UnlockEgKScreen( } } - val onClose: () -> Unit = { navController.popBackStack() } - val onBack: () -> Unit = { unlockNavController.popBackStack() } - NavHost( unlockNavController, startDestination = UnlockEgkNavigation.Intro.path() @@ -140,7 +123,7 @@ fun UnlockEgKScreen( cardAccessNumber = cardAccessNumber, onCanChanged = { cardAccessNumber = it }, onClickLearnMore = { onClickLearnMore() }, - onCancel = onClose + onCancel = onCancel ) { if (unlockMethod == UnlockMethod.ChangeReferenceData) { unlockNavController.navigate(UnlockEgkNavigation.OldSecret.path()) @@ -150,14 +133,13 @@ fun UnlockEgKScreen( } } } - composable(UnlockEgkNavigation.PersonalUnblockingKey.route) { NavigationAnimation { PersonalUnblockingKeyScreen( personalUnblockingKey = personalUnblockingKey, unlockMethod = unlockMethod, onPersonalUnblockingKeyChanged = { personalUnblockingKey = it }, - onCancel = onClose + onCancel = onCancel ) { if (unlockMethod == UnlockMethod.ResetRetryCounterWithNewSecret) { unlockNavController.navigate(UnlockEgkNavigation.NewSecret.path()) @@ -167,31 +149,28 @@ fun UnlockEgKScreen( } } } - composable(UnlockEgkNavigation.OldSecret.route) { NavigationAnimation { OldSecretScreen( oldSecret = oldSecret, onSecretChange = { oldSecret = it }, - onCancel = onClose + onCancel = onCancel ) { unlockNavController.navigate(UnlockEgkNavigation.NewSecret.path()) } } } - composable(UnlockEgkNavigation.NewSecret.route) { NavigationAnimation { NewSecretScreen( newSecret = newSecret, onSecretChange = { newSecret = it }, - onCancel = onClose + onCancel = onCancel ) { unlockNavController.navigate(UnlockEgkNavigation.UnlockEgk.path()) } } } - composable(UnlockEgkNavigation.UnlockEgk.route) { NavigationAnimation { UnlockScreen( @@ -201,87 +180,46 @@ fun UnlockEgKScreen( personalUnblockingKey = personalUnblockingKey, oldSecret = oldSecret, newSecret = newSecret, - onBack = onBack, + onBack = { unlockNavController.popBackStack() }, onClickTroubleshooting = { - unlockNavController.navigate(UnlockEgkNavigation.TroubleshootingPageA.path()) + unlockNavController.navigate(UnlockEgkNavigation.TroubleShooting.path()) }, onRetryCan = onRetryCan, onRetryOldSecret = onRetryOldSecret, onRetryPuk = onRetryPuk, - onFinishUnlock = onClose, + onFinishUnlock = onCancel, onAssignPin = onAssignPin ) } } - - composable(UnlockEgkNavigation.TroubleshootingPageA.route) { + composable(UnlockEgkNavigation.TroubleShooting.route) { NavigationAnimation { - UnlockEGKTroubleshootingPageA( - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryCan = onRetryCan, - onRetryOldSecret = onRetryOldSecret, - onRetryPuk = onRetryPuk, - onFinishUnlock = onClose, - onAssignPin = onAssignPin, - onNext = { unlockNavController.navigate(UnlockEgkNavigation.TroubleshootingPageB.path()) }, - onBack = onBack + TroubleShootingScreen( + onClickTryMe = { + unlockNavController.navigate(UnlockEgkNavigation.UnlockEgk.path()) + }, + onCancel = { unlockNavController.popBackStack() } ) } } + } +} - composable(UnlockEgkNavigation.TroubleshootingPageB.route) { - NavigationAnimation { - UnlockEGKTroubleshootingPageB( - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryCan = onRetryCan, - onRetryOldSecret = onRetryOldSecret, - onRetryPuk = onRetryPuk, - onFinishUnlock = onClose, - onAssignPin = onAssignPin, - onNext = { unlockNavController.navigate(UnlockEgkNavigation.TroubleshootingPageC.path()) }, - onBack = onBack - ) - } - } +private fun onRetryPuk(unlockNavController: NavHostController): () -> Unit = { + unlockNavController.navigate(UnlockEgkNavigation.PersonalUnblockingKey.path()) { + popUpTo(UnlockEgkNavigation.PersonalUnblockingKey.path()) { inclusive = true } + } +} - composable(UnlockEgkNavigation.TroubleshootingPageC.route) { - NavigationAnimation { - UnlockEGKTroubleshootingPageC( - viewModel = viewModel, - unlockMethod = unlockMethod, - cardAccessNumber = cardAccessNumber, - personalUnblockingKey = personalUnblockingKey, - oldSecret = oldSecret, - newSecret = newSecret, - onRetryCan = onRetryCan, - onRetryOldSecret = onRetryOldSecret, - onRetryPuk = onRetryPuk, - onFinishUnlock = onClose, - onAssignPin = onAssignPin, - onNext = { unlockNavController.navigate(UnlockEgkNavigation.TroubleshootingNoSuccessPage.path()) }, - onBack = onBack - ) - } - } +private fun onRetryOldSecret(unlockNavController: NavHostController): () -> Unit = { + unlockNavController.navigate(UnlockEgkNavigation.OldSecret.path()) { + popUpTo(UnlockEgkNavigation.OldSecret.path()) { inclusive = true } + } +} - composable(UnlockEgkNavigation.TroubleshootingNoSuccessPage.route) { - NavigationAnimation { - UnlockEGKTroubleshootingNoSuccessPage( - onNext = onClose, - onBack = onBack - ) - } - } +private fun onRetryCan(unlockNavController: NavHostController): () -> Unit = { + unlockNavController.navigate(UnlockEgkNavigation.CardAccessNumber.path()) { + popUpTo(UnlockEgkNavigation.CardAccessNumber.path()) { inclusive = true } } } @@ -530,8 +468,10 @@ private fun NewSecretScreen( ) { val secretRange = SECRET_MIN_LENGTH..SECRET_MAX_LENGTH var repeatedNewSecret by remember { mutableStateOf("") } - val isConsistent by derivedStateOf { - repeatedNewSecret.isNotBlank() && newSecret == repeatedNewSecret + val isConsistent by remember { + derivedStateOf { + repeatedNewSecret.isNotBlank() && newSecret == repeatedNewSecret + } } val lazyListState = rememberLazyListState() 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 39d6a52b..52021d09 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 @@ -51,7 +51,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties @@ -65,18 +64,17 @@ import de.gematik.ti.erp.app.cardunlock.usecase.UnlockEgkState import de.gematik.ti.erp.app.cardwall.ui.CardAnimationBox import de.gematik.ti.erp.app.cardwall.ui.EnableNfcDialog import de.gematik.ti.erp.app.cardwall.ui.ErrorDialog -import de.gematik.ti.erp.app.cardwall.ui.Troubleshooting +import de.gematik.ti.erp.app.cardwall.ui.InfoText import de.gematik.ti.erp.app.cardwall.ui.pinRetriesLeft import de.gematik.ti.erp.app.cardwall.ui.rotatingScanCardAssistance -import de.gematik.ti.erp.app.cardwall.ui.toAnnotatedString import de.gematik.ti.erp.app.core.LocalActivity 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 import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AcceptDialog import de.gematik.ti.erp.app.utils.compose.Dialog import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -523,24 +521,14 @@ fun CardCommunicationDialog( else -> info } - if (showTroubleshooting) { - Troubleshooting( - onClick = { onClickTroubleshooting?.run { onClickTroubleshooting() } } - ) - } else { - Text( - info.first, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Text( - info.second, - style = AppTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } + + InfoText( + showTroubleshooting, + info, + onClickTroubleshooting = { + onClickTroubleshooting?.run { onClickTroubleshooting() } + } + ) } } } 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 6eac0d70..646e5f12 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 @@ -75,7 +75,6 @@ import de.gematik.ti.erp.app.cardwall.ui.ReadingCardAnimation import de.gematik.ti.erp.app.cardwall.ui.SearchingCardAnimation import de.gematik.ti.erp.app.cardwall.ui.TagLostCard import de.gematik.ti.erp.app.cardwall.ui.pinRetriesLeft -import de.gematik.ti.erp.app.cardwall.ui.toAnnotatedString import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData @@ -85,6 +84,7 @@ import de.gematik.ti.erp.app.utils.compose.AcceptDialog import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.Dialog import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -371,7 +371,9 @@ private fun HealthCardCredentials( ) { var pin by remember { mutableStateOf("") } var pinVisible by remember { mutableStateOf(false) } - val pinCorrect by derivedStateOf { pin.matches(PinCorrectRegex) } + val pinCorrect by remember { + derivedStateOf { pin.matches(PinCorrectRegex) } + } Column( modifier = modifier, diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt index 2333200d..f6a4d0b5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt @@ -32,10 +32,10 @@ import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.cardwall.ui.toAnnotatedString import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.utils.compose.AcceptDialog +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt new file mode 100644 index 00000000..71b27f7e --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt @@ -0,0 +1,338 @@ +/* + * 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.cardwall.ui + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateValue +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.theme.AppTheme +import kotlinx.coroutines.delay + +enum class HealthCardAnimationState { + START, + ZOOM_OUT, + POSITION_1, + POSITION_2, + POSITION_3 +} +private data class Wobble(val radius: Dp, val color: Color, val delay: Int) + +@Suppress("MagicNumber") +@Composable +fun SearchingCardAnimation() { + val wobbleColorL = Wobble(72.dp, AppTheme.colors.primary100.copy(alpha = 0.7f), 600) + val wobbleColorM = Wobble(56.dp, AppTheme.colors.primary200.copy(alpha = 0.3f), 300) + val wobbleColorS = Wobble(40.dp, AppTheme.colors.primary300.copy(alpha = 0.2f), 0) + + val wobble = listOf(wobbleColorL, wobbleColorM, wobbleColorS) + + val wobbleTransition = rememberInfiniteTransition() + val slowInSlowOut = CubicBezierEasing(0.3f, 0.0f, 0.7f, 1.0f) + + val smartPhone = painterResource(R.drawable.ic_phone_transparent) + val healthCard = painterResource(R.drawable.ic_healthcard) + + var smartPhoneToggle by remember { mutableStateOf(false) } + var healthCardToggle by remember { mutableStateOf(HealthCardAnimationState.START) } + + val smartPhoneTransition = updateTransition(smartPhoneToggle, label = "SmartPhoneToggle") + val healthCardTransition = updateTransition(healthCardToggle, label = "HealthCardToggle") + + val healthCardOffsetDuration = 1500 + + val healthCardOffset by calculateHealthCardOffset(healthCardTransition, healthCardOffsetDuration) + val healthCardScale by calculateHealthCardScale(healthCardTransition) + val smartPhoneAlpha by calculateSmartPhoneAlpha(smartPhoneTransition) + val smartPhoneOffset by calculateSmartPhoneOffset(smartPhoneTransition) + + SideEffect { + smartPhoneToggle = true + } + + LaunchedEffect(Unit) { + delay(3000) + healthCardToggle = HealthCardAnimationState.ZOOM_OUT + while (true) { + delay(healthCardOffsetDuration.toLong()) + healthCardToggle = HealthCardAnimationState.POSITION_1 + delay(healthCardOffsetDuration.toLong()) + healthCardToggle = HealthCardAnimationState.POSITION_2 + delay(healthCardOffsetDuration.toLong()) + healthCardToggle = HealthCardAnimationState.POSITION_3 + } + } + + val wobbleAnimations = + wobble.map { + Triple( + it, + wobbleTransition.animateFloat( + 1.0f, + 1.1f, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 2500 + 1.0f at 0 + 1.0f at it.delay with slowInSlowOut + 1.1f at 1000 + it.delay + 1.0f at 2500 + }, + repeatMode = RepeatMode.Restart + ) + ), + wobbleTransition.animateFloat( + 1.0f, + 0.7f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, + delayMillis = it.delay, + slowInSlowOut + ), + repeatMode = RepeatMode.Reverse + ) + ) + ) + } + + Box { + Box( + modifier = Modifier + .drawBehind { + wobbleAnimations.forEach { (wobble, animScale, animAlpha) -> + drawCircle( + color = wobble.color, + radius = wobble.radius.toPx() * animScale.value, + alpha = animAlpha.value + ) + } + } + ) { + Image( + healthCard, + null, + modifier = Modifier + .size(100.dp) + .graphicsLayer { + scaleX = healthCardScale + scaleY = healthCardScale + } + .offset(healthCardOffset.x, healthCardOffset.y) + .align(Alignment.Center) + ) + + Image( + smartPhone, + null, + alpha = smartPhoneAlpha, + modifier = Modifier + .size(80.dp) + .align( + Alignment.Center + ) + .offset(y = smartPhoneOffset) + ) + } + } +} + +@Suppress("MagicNumber") +@Composable +private fun calculateSmartPhoneOffset(smartPhoneTransition: Transition): State { + val smartPhoneOffset = smartPhoneTransition.animateDp( + transitionSpec = { + tween( + 1300, + 1500 + ) + }, + label = "smartPhoneOffset" + ) { state -> + when (state) { + true -> 0.dp + false -> 50.dp + } + } + return smartPhoneOffset +} + +@Suppress("MagicNumber") +@Composable +private fun calculateSmartPhoneAlpha(smartPhoneTransition: Transition): State { + val smartPhoneAlpha = smartPhoneTransition.animateFloat( + transitionSpec = { + tween( + 1300, + 1500 + ) + }, + label = "smartPhoneAlpha" + ) { state -> + when (state) { + true -> 1.0f + false -> 0.0f + } + } + return smartPhoneAlpha +} + +@Suppress("MagicNumber") +@Composable +private fun calculateHealthCardScale(healthCardTransition: Transition): State { + val healthCardScale = healthCardTransition.animateFloat( + transitionSpec = { + tween( + 1000, + 0 + ) + }, + label = "healthCardScale" + ) { state -> + when (state) { + HealthCardAnimationState.START -> 1.0f + else -> 0.7f + } + } + return healthCardScale +} + +@Suppress("MagicNumber") +@Composable +private fun calculateHealthCardOffset( + healthCardTransition: Transition, + healthCardOffsetDuration: Int +): State { + val healthCardOffset = healthCardTransition.animateValue( + DpOffset.VectorConverter, + transitionSpec = { + tween( + healthCardOffsetDuration - 10, + 0 + ) + }, + label = "healthCardOffset" + ) { state -> + when (state) { + HealthCardAnimationState.START -> DpOffset(0.dp, 0.dp) + HealthCardAnimationState.ZOOM_OUT -> DpOffset(-(30.dp), 0.dp) + HealthCardAnimationState.POSITION_1 -> DpOffset(30.dp, 0.dp) + HealthCardAnimationState.POSITION_2 -> DpOffset(-(20.dp), 30.dp) + HealthCardAnimationState.POSITION_3 -> DpOffset(-(30.dp), 0.dp) + } + } + return healthCardOffset +} + +@Composable +fun ReadingCardAnimation() { + Box { + Image( + painterResource(R.drawable.ic_healthcard_spinner), + null, + modifier = Modifier + .align( + Alignment.CenterEnd + ) + ) + CircularProgressIndicator( + color = AppTheme.colors.neutral400, + strokeWidth = 2.dp, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(4.dp) + .size(24.dp) + ) + } +} + +@Composable +fun TagLostCard() { + Image( + painterResource(R.drawable.ic_healthcard_tag_lost), + null + ) +} + +@Composable +fun CardAnimationBox(screen: Int) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .defaultMinSize(minHeight = 150.dp) + .fillMaxWidth() + ) { + when (screen) { + 0 -> SearchingCardAnimation() + 1 -> ReadingCardAnimation() + 2 -> TagLostCard() + } + } +} + +@Composable +fun rotatingScanCardAssistance() = listOf( + Pair( + stringResource(R.string.cdw_nfc_search1_headline), + stringResource(R.string.cdw_nfc_search1_info) + ), + Pair( + stringResource(R.string.cdw_nfc_search2_headline), + stringResource(R.string.cdw_nfc_search2_info) + ), + Pair( + stringResource(R.string.cdw_nfc_search3_headline), + stringResource(R.string.cdw_nfc_search3_info) + ) +) 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 1cd152a7..b6ba9731 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 @@ -20,45 +20,23 @@ package de.gematik.ti.erp.app.cardwall.ui import android.content.Intent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.VectorConverter -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.animateValue -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.keyframes -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -68,12 +46,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString @@ -82,8 +57,6 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.compose.foundation.layout.systemBarsPadding @@ -98,14 +71,13 @@ import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.analytics.Analytics -import de.gematik.ti.erp.app.analytics.Analytics.AuthenticationProblem +import de.gematik.ti.erp.app.analytics.trackAuth +import de.gematik.ti.erp.app.troubleShooting.TroubleshootingInfo import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.Dialog -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -122,14 +94,6 @@ import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException -private enum class HealthCardAnimationState { - START, - ZOOM_OUT, - POSITION_1, - POSITION_2, - POSITION_3 -} - @Stable class CardWallAuthenticationDialogState { val toggleAuth = MutableSharedFlow() @@ -152,7 +116,7 @@ fun rememberCardWallAuthenticationDialogState(): CardWallAuthenticationDialogSta @Composable fun CardWallAuthenticationDialog( dialogState: CardWallAuthenticationDialogState = rememberCardWallAuthenticationDialogState(), - viewModel: CardWallViewModel, + cardWallController: CardWallController, authenticationData: CardWallAuthenticationData, profileId: ProfileIdentifier, troubleShootingEnabled: Boolean = false, @@ -182,7 +146,7 @@ fun CardWallAuthenticationDialog( is ToggleAuth.ToggleByUser -> { if (it.value) { emitAll( - viewModel.doAuthentication( + cardWallController.doAuthentication( profileId = profileId, authenticationData = authenticationData, activity @@ -211,7 +175,7 @@ fun CardWallAuthenticationDialog( } } emitAll( - viewModel.doAuthentication( + cardWallController.doAuthentication( profileId = profileId, authenticationData = authenticationData, tagFlow @@ -285,15 +249,43 @@ fun CardWallAuthenticationDialog( ) } - val nextText = when (state) { - AuthenticationState.HealthCardCardAccessNumberWrong -> stringResource(R.string.cdw_auth_retry_pin_can) - AuthenticationState.HealthCardPin2RetriesLeft, - AuthenticationState.HealthCardPin1RetryLeft -> stringResource(R.string.cdw_auth_retry_pin_can) - AuthenticationState.HealthCardBlocked -> stringResource(R.string.cdw_auth_retry_unlock_egk) - else -> stringResource(R.string.cdw_auth_retry) + val nextText = extractNextText(state) + val retryText = extractRetryText(state) + + if (showEnableNfcDialog) { + EnableNfcDialog { + showEnableNfcDialog = false + } + } + + retryText?.let { + ErrorDialog( + header = it.first, + info = it.second, + retryButtonText = nextText, + onCancel = { + coroutineScope.launch { toggleAuth.emit(ToggleAuth.ToggleByUser(false)) } + }, + onRetry = { + when (state) { + AuthenticationState.HealthCardCardAccessNumberWrong -> onRetryCan() + AuthenticationState.HealthCardPin2RetriesLeft, + AuthenticationState.HealthCardPin1RetryLeft -> onRetryPin() + AuthenticationState.HealthCardBlocked -> onUnlockEgk() + else -> if (cardWallController.isNFCEnabled()) { + coroutineScope.launch { + toggleAuth.emit(ToggleAuth.ToggleByUser(true)) + } + } + } + } + ) } +} - val retryText = when (val s = state) { +@Composable +fun extractRetryText(state: AuthenticationState): Pair? = + when (val s = state) { AuthenticationState.IDPCommunicationFailed -> Pair( stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() @@ -337,36 +329,15 @@ fun CardWallAuthenticationDialog( else -> null } - if (showEnableNfcDialog) { - EnableNfcDialog { - showEnableNfcDialog = false - } - } - - retryText?.let { - ErrorDialog( - header = it.first, - info = it.second, - retryButtonText = nextText, - onCancel = { - coroutineScope.launch { toggleAuth.emit(ToggleAuth.ToggleByUser(false)) } - }, - onRetry = { - when (state) { - AuthenticationState.HealthCardCardAccessNumberWrong -> onRetryCan() - AuthenticationState.HealthCardPin2RetriesLeft, - AuthenticationState.HealthCardPin1RetryLeft -> onRetryPin() - AuthenticationState.HealthCardBlocked -> onUnlockEgk() - else -> if (viewModel.isNFCEnabled()) { - coroutineScope.launch { - toggleAuth.emit(ToggleAuth.ToggleByUser(true)) - } - } - } - } - ) +@Composable +fun extractNextText(state: AuthenticationState): String = + when (state) { + AuthenticationState.HealthCardCardAccessNumberWrong -> stringResource(R.string.cdw_auth_retry_pin_can) + AuthenticationState.HealthCardPin2RetriesLeft, + AuthenticationState.HealthCardPin1RetryLeft -> stringResource(R.string.cdw_auth_retry_pin_can) + AuthenticationState.HealthCardBlocked -> stringResource(R.string.cdw_auth_retry_unlock_egk) + else -> stringResource(R.string.cdw_auth_retry) } -} @Composable fun EnableNfcDialog(onCancel: () -> Unit) { @@ -405,9 +376,6 @@ fun ErrorDialog( actionText = retryButtonText ) -fun String.toAnnotatedString() = - buildAnnotatedString { append(this@toAnnotatedString) } - @Composable fun pinRetriesLeft(count: Int) = annotatedPluralsResource( @@ -498,52 +466,15 @@ private fun AuthenticationDialog( } } - info = when (state) { - AuthenticationState.HealthCardCommunicationChannelReady -> Pair( - stringResource(R.string.cdw_nfc_found_headline), - stringResource(R.string.cdw_nfc_found_info) - ) - AuthenticationState.HealthCardCommunicationTrustedChannelEstablished -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_trusted_channel_established), - stringResource(R.string.cdw_nfc_communication_info) - ) - AuthenticationState.HealthCardCommunicationFinished -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_certificate_loaded), - stringResource(R.string.cdw_nfc_communication_info) - ) - AuthenticationState.IDPCommunicationFinished -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_pin_verified), - stringResource(R.string.cdw_nfc_communication_info) - ) - AuthenticationState.AuthenticationFlowFinished -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_challenge_signed), - stringResource(R.string.cdw_nfc_communication_info) - ) - AuthenticationState.HealthCardCommunicationInterrupted -> Pair( - stringResource(R.string.cdw_nfc_tag_lost_headline), - stringResource(R.string.cdw_nfc_tag_lost_info) - ) - else -> info - } + info = extractInfo(state) ?: info - if (showTroubleshooting) { - Troubleshooting( - onClick = { onClickTroubleshooting?.run { onClickTroubleshooting() } } - ) - } else { - Text( - info.first, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Text( - info.second, - style = AppTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } + InfoText( + showTroubleshooting, + info, + onClickTroubleshooting = { + onClickTroubleshooting?.run { onClickTroubleshooting() } + } + ) } } } @@ -551,299 +482,53 @@ private fun AuthenticationDialog( } @Composable -fun Troubleshooting( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { +fun InfoText(showTroubleshooting: Boolean, info: Pair, onClickTroubleshooting: () -> Unit?) { + if (showTroubleshooting) { + TroubleshootingInfo( + onClick = { onClickTroubleshooting?.run { onClickTroubleshooting() } } + ) + } else { Text( - stringResource(R.string.cdw_enter_troubleshooting_title), + info.first, style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() ) Text( - stringResource(R.string.cdw_enter_troubleshooting_subtitle), + info.second, style = AppTheme.typography.body2, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() ) - SpacerMedium() - Button( - onClick = onClick, - shape = RoundedCornerShape(8.dp), - elevation = ButtonDefaults.elevation(defaultElevation = 0.dp), - contentPadding = PaddingValues(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny) - ) { - Icon(Icons.Outlined.Lightbulb, null) - SpacerTiny() - Text(stringResource(R.string.cdw_enter_troubleshooting_action)) - } - } -} - -private data class Wobble(val radius: Dp, val color: Color, val delay: Int) - -@Suppress("LongMethod") -@Composable -fun SearchingCardAnimation() { - val wobbleColorL = Wobble(72.dp, AppTheme.colors.primary100.copy(alpha = 0.7f), 600) - val wobbleColorM = Wobble(56.dp, AppTheme.colors.primary200.copy(alpha = 0.3f), 300) - val wobbleColorS = Wobble(40.dp, AppTheme.colors.primary300.copy(alpha = 0.2f), 0) - - val wobble = listOf(wobbleColorL, wobbleColorM, wobbleColorS) - - val wobbleTransition = rememberInfiniteTransition() - val slowInSlowOut = CubicBezierEasing(0.3f, 0.0f, 0.7f, 1.0f) - - val smartPhone = painterResource(R.drawable.ic_phone_transparent) - val healthCard = painterResource(R.drawable.ic_healthcard) - - var smartPhoneToggle by remember { mutableStateOf(false) } - var healthCardToggle by remember { mutableStateOf(HealthCardAnimationState.START) } - - val smartPhoneTransition = updateTransition(smartPhoneToggle) - val healthCardTransition = updateTransition(healthCardToggle) - - val healthCardOffsetDuration = 1500 - - val healthCardOffset by healthCardTransition.animateValue( - DpOffset.VectorConverter, - transitionSpec = { - tween( - healthCardOffsetDuration - 10, - 0 - ) - }, - label = "healthCardOffset" - ) { state -> - when (state) { - HealthCardAnimationState.START -> DpOffset(0.dp, 0.dp) - HealthCardAnimationState.ZOOM_OUT -> DpOffset(-(30.dp), 0.dp) - HealthCardAnimationState.POSITION_1 -> DpOffset(30.dp, 0.dp) - HealthCardAnimationState.POSITION_2 -> DpOffset(-(20.dp), 30.dp) - HealthCardAnimationState.POSITION_3 -> DpOffset(-(30.dp), 0.dp) - } - } - - val healthCardScale by healthCardTransition.animateFloat( - transitionSpec = { - tween( - 1000, - 0 - ) - }, - label = "healthCardScale" - ) { state -> - when (state) { - HealthCardAnimationState.START -> 1.0f - else -> 0.7f - } - } - val smartPhoneAlpha by smartPhoneTransition.animateFloat( - transitionSpec = { - tween( - 1300, - 1500 - ) - }, - label = "smartPhoneAlpha" - ) { state -> - when (state) { - true -> 1.0f - false -> 0.0f - } - } - - val smartPhoneOffset by smartPhoneTransition.animateDp( - transitionSpec = { - tween( - 1300, - 1500 - ) - }, - label = "smartPhoneOffset" - ) { state -> - when (state) { - true -> 0.dp - false -> 50.dp - } - } - - SideEffect { - smartPhoneToggle = true - } - - LaunchedEffect(Unit) { - delay(3000) - healthCardToggle = HealthCardAnimationState.ZOOM_OUT - while (true) { - delay(healthCardOffsetDuration.toLong()) - healthCardToggle = HealthCardAnimationState.POSITION_1 - delay(healthCardOffsetDuration.toLong()) - healthCardToggle = HealthCardAnimationState.POSITION_2 - delay(healthCardOffsetDuration.toLong()) - healthCardToggle = HealthCardAnimationState.POSITION_3 - } - } - - val wobbleAnimations = - wobble.map { - Triple( - it, - wobbleTransition.animateFloat( - 1.0f, - 1.1f, - animationSpec = infiniteRepeatable( - animation = keyframes { - durationMillis = 2500 - 1.0f at 0 - 1.0f at it.delay with slowInSlowOut - 1.1f at 1000 + it.delay - 1.0f at 2500 - }, - repeatMode = RepeatMode.Restart - ) - ), - wobbleTransition.animateFloat( - 1.0f, - 0.7f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, - delayMillis = it.delay, - slowInSlowOut - ), - repeatMode = RepeatMode.Reverse - ) - ) - ) - } - - Box { - Box( - modifier = Modifier - .drawBehind { - wobbleAnimations.forEach { (wobble, animScale, animAlpha) -> - drawCircle( - color = wobble.color, - radius = wobble.radius.toPx() * animScale.value, - alpha = animAlpha.value - ) - } - } - ) { - Image( - healthCard, - null, - modifier = Modifier - .size(100.dp) - .graphicsLayer { - scaleX = healthCardScale - scaleY = healthCardScale - } - .offset(healthCardOffset.x, healthCardOffset.y) - .align(Alignment.Center) - ) - - Image( - smartPhone, - null, - alpha = smartPhoneAlpha, - modifier = Modifier - .size(80.dp) - .align( - Alignment.Center - ) - .offset(y = smartPhoneOffset) - ) - } } } @Composable -fun ReadingCardAnimation() { - Box { - Image( - painterResource(R.drawable.ic_healthcard_spinner), - null, - modifier = Modifier - .align( - Alignment.CenterEnd - ) +fun extractInfo(state: AuthenticationState): Pair? = + when (state) { + AuthenticationState.HealthCardCommunicationChannelReady -> Pair( + stringResource(R.string.cdw_nfc_found_headline), + stringResource(R.string.cdw_nfc_found_info) ) - CircularProgressIndicator( - color = AppTheme.colors.neutral400, - strokeWidth = 2.dp, - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(4.dp) - .size(24.dp) + AuthenticationState.HealthCardCommunicationTrustedChannelEstablished -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_trusted_channel_established), + stringResource(R.string.cdw_nfc_communication_info) ) + AuthenticationState.HealthCardCommunicationFinished -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_certificate_loaded), + stringResource(R.string.cdw_nfc_communication_info) + ) + AuthenticationState.IDPCommunicationFinished -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_pin_verified), + stringResource(R.string.cdw_nfc_communication_info) + ) + AuthenticationState.AuthenticationFlowFinished -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_challenge_signed), + stringResource(R.string.cdw_nfc_communication_info) + ) + AuthenticationState.HealthCardCommunicationInterrupted -> Pair( + stringResource(R.string.cdw_nfc_tag_lost_headline), + stringResource(R.string.cdw_nfc_tag_lost_info) + ) + else -> null } -} - -@Composable -fun TagLostCard() { - Image( - painterResource(R.drawable.ic_healthcard_tag_lost), - null - ) -} - -private fun Analytics.trackAuth(state: AuthenticationState) { - if (trackingAllowed.value) { - when (state) { - AuthenticationState.HealthCardBlocked -> - trackAuthenticationProblem(AuthenticationProblem.CardBlocked) - AuthenticationState.HealthCardCardAccessNumberWrong -> - trackAuthenticationProblem(AuthenticationProblem.CardAccessNumberWrong) - AuthenticationState.HealthCardCommunicationInterrupted -> - trackAuthenticationProblem(AuthenticationProblem.CardCommunicationInterrupted) - AuthenticationState.HealthCardPin1RetryLeft, - AuthenticationState.HealthCardPin2RetriesLeft -> - trackAuthenticationProblem(AuthenticationProblem.CardPinWrong) - AuthenticationState.IDPCommunicationFailed -> - trackAuthenticationProblem(AuthenticationProblem.IDPCommunicationFailed) - AuthenticationState.IDPCommunicationInvalidCertificate -> - trackAuthenticationProblem(AuthenticationProblem.IDPCommunicationInvalidCertificate) - AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> - trackAuthenticationProblem(AuthenticationProblem.IDPCommunicationInvalidOCSPOfCard) - AuthenticationState.SecureElementCryptographyFailed -> - trackAuthenticationProblem(AuthenticationProblem.SecureElementCryptographyFailed) - AuthenticationState.UserNotAuthenticated -> - trackAuthenticationProblem(AuthenticationProblem.UserNotAuthenticated) - else -> {} - } - } -} - -@Composable -fun CardAnimationBox(screen: Int) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .defaultMinSize(minHeight = 150.dp) - .fillMaxWidth() - ) { - when (screen) { - 0 -> SearchingCardAnimation() - 1 -> ReadingCardAnimation() - 2 -> TagLostCard() - } - } -} - -@Composable -fun rotatingScanCardAssistance() = listOf( - Pair( - stringResource(R.string.cdw_nfc_search1_headline), - stringResource(R.string.cdw_nfc_search1_info) - ), - Pair( - stringResource(R.string.cdw_nfc_search2_headline), - stringResource(R.string.cdw_nfc_search2_info) - ), - Pair( - stringResource(R.string.cdw_nfc_search3_headline), - stringResource(R.string.cdw_nfc_search3_info) - ) -) 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 c690bcfa..57ee684c 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 @@ -49,7 +49,6 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.material.icons.rounded.RadioButtonUnchecked import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -76,6 +75,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +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.cardunlock.ui.UnlockEgKScreen @@ -90,7 +91,8 @@ import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.prescription.detail.ui.rememberNotSaveableNavController +import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.troubleShooting.TroubleShootingScreen import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.HintCard @@ -107,7 +109,6 @@ import de.gematik.ti.erp.app.utils.compose.annotatedLinkString import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.navigationModeState import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberViewModel @Composable fun CardWallScreen( @@ -115,14 +116,11 @@ fun CardWallScreen( onResumeCardWall: () -> Unit, profileId: ProfileIdentifier ) { - val viewModel: CardWallViewModel by rememberViewModel() - - val navController = rememberNotSaveableNavController() - - val state by viewModel.state().collectAsState(viewModel.defaultState) + val cardWallController = rememberCardWallController() + val navController = rememberNavController() val startDestination = when { - state.hardwareRequirementsFulfilled -> CardWallNavigation.Intro.path() + cardWallController.hardwareRequirementsFulfilled -> CardWallNavigation.Intro.path() else -> CardWallNavigation.MissingCapabilities.path() } @@ -142,20 +140,6 @@ fun CardWallScreen( } ) - val onRetryCan = { - navController.navigate(CardWallNavigation.CardAccessNumber.path()) { - popUpTo(CardWallNavigation.CardAccessNumber.path()) { inclusive = true } - } - } - - val onRetryPin = { - navController.navigate(CardWallNavigation.PersonalIdentificationNumber.path()) { - popUpTo(CardWallNavigation.PersonalIdentificationNumber.path()) { - inclusive = true - } - } - } - val onUnlockEgk = { navController.navigate(CardWallNavigation.UnlockEgk.path()) { popUpTo(CardWallNavigation.UnlockEgk.path()) { @@ -174,21 +158,26 @@ fun CardWallScreen( TrackNavigationChanges(navController) - var cardAccessNumber by remember { mutableStateOf("") } - var personalIdentificationNumber by remember { mutableStateOf("") } - var altPairingInitialState: AltPairingProvider.AuthResult? by remember { mutableStateOf(null) } - - val authenticationData by derivedStateOf { - (altPairingInitialState as? AltPairingProvider.AuthResult.Initialized)?.let { - CardWallAuthenticationData.AltPairingWithHealthCard( + var cardAccessNumber + by rememberSaveable { mutableStateOf("") } + var personalIdentificationNumber + by rememberSaveable { mutableStateOf("") } + var altPairingInitialState: AltPairingProvider.AuthResult? + by rememberSaveable(saver = complexAutoSaver()) { mutableStateOf(null) } + + val authenticationData by remember(altPairingInitialState) { + derivedStateOf { + (altPairingInitialState as? AltPairingProvider.AuthResult.Initialized)?.let { + CardWallAuthenticationData.AltPairingWithHealthCard( + cardAccessNumber = cardAccessNumber, + personalIdentificationNumber = personalIdentificationNumber, + initialPairingData = it + ) + } ?: CardWallAuthenticationData.HealthCard( cardAccessNumber = cardAccessNumber, - personalIdentificationNumber = personalIdentificationNumber, - initialPairingData = it + personalIdentificationNumber = personalIdentificationNumber ) - } ?: CardWallAuthenticationData.HealthCard( - cardAccessNumber = cardAccessNumber, - personalIdentificationNumber = personalIdentificationNumber - ) + } } NavHost( @@ -307,6 +296,7 @@ fun CardWallScreen( } ) } + else -> { onBack() } @@ -323,7 +313,7 @@ fun CardWallScreen( composable(CardWallNavigation.Authentication.route) { NavigationAnimation(mode = navigationMode) { CardWallNfcInstructionScreen( - viewModel = viewModel, + cardWallController = cardWallController, profileId = profileId, authenticationData = authenticationData, onNext = { @@ -343,7 +333,7 @@ fun CardWallScreen( }, onUnlockEgk = onUnlockEgk, onClickTroubleshooting = { - navController.navigate(CardWallNavigation.TroubleshootingPageA.path()) + navController.navigate(CardWallNavigation.Troubleshooting.path()) }, onBack = onBack ) @@ -361,59 +351,13 @@ fun CardWallScreen( } } - composable(CardWallNavigation.TroubleshootingPageA.route) { - NavigationAnimation(mode = navigationMode) { - CardWallTroubleshootingPageA( - profileId = profileId, - viewModel = viewModel, - onFinal = onResumeCardWall, - onBack = onBack, - onNext = { navController.navigate(CardWallNavigation.TroubleshootingPageB.path()) }, - authenticationData = authenticationData, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - } - } - - composable(CardWallNavigation.TroubleshootingPageB.route) { - NavigationAnimation(mode = navigationMode) { - CardWallTroubleshootingPageB( - profileId = profileId, - viewModel = viewModel, - onFinal = onResumeCardWall, - onBack = onBack, - onNext = { navController.navigate(CardWallNavigation.TroubleshootingPageC.path()) }, - authenticationData = authenticationData, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - } - } - - composable(CardWallNavigation.TroubleshootingPageC.route) { + composable(CardWallNavigation.Troubleshooting.route) { NavigationAnimation(mode = navigationMode) { - CardWallTroubleshootingPageC( - profileId = profileId, - viewModel = viewModel, - onFinal = onResumeCardWall, - onBack = onBack, - onNext = { navController.navigate(CardWallNavigation.TroubleshootingNoSuccessPage.path()) }, - authenticationData = authenticationData, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - } - } - - composable(CardWallNavigation.TroubleshootingNoSuccessPage.route) { - NavigationAnimation(mode = navigationMode) { - CardWallTroubleshootingNoSuccessPage( - onBack = onBack, - onNext = onResumeCardWall + TroubleShootingScreen( + onClickTryMe = { + navController.navigate(CardWallNavigation.Authentication.path()) + }, + onCancel = { navController.popBackStack() } ) } } @@ -422,7 +366,7 @@ fun CardWallScreen( NavigationAnimation(mode = navigationMode) { UnlockEgKScreen( unlockMethod = UnlockMethod.ResetRetryCounter, - navController = mainNavController, + onCancel = { mainNavController.popBackStack() }, onClickLearnMore = { navController.navigate(CardWallNavigation.OrderHealthCard.path()) } ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt similarity index 83% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallViewModel.kt rename to android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt index bbfeb671..b755114f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallViewModel.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt @@ -20,33 +20,30 @@ package de.gematik.ti.erp.app.cardwall.ui import android.nfc.Tag import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcHealthCard -import de.gematik.ti.erp.app.cardwall.ui.model.CardWallData import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase -import androidx.lifecycle.ViewModel import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.kodein.di.compose.rememberInstance -class CardWallViewModel( +@Stable +class CardWallController( private val cardWallUseCase: CardWallUseCase, private val authenticationUseCase: AuthenticationUseCase, private val dispatchers: DispatchProvider -) : ViewModel() { - - val defaultState = CardWallData.State( - hardwareRequirementsFulfilled = cardWallUseCase.deviceHasNFCAndAndroidMOrHigher - ) - - fun state(): Flow = flowOf(defaultState) +) { + val hardwareRequirementsFulfilled = cardWallUseCase.deviceHasNFCAndAndroidMOrHigher fun doAuthentication( profileId: ProfileIdentifier, @@ -93,3 +90,17 @@ class CardWallViewModel( fun isNFCEnabled() = cardWallUseCase.deviceHasNFCEnabled } + +@Composable +fun rememberCardWallController(): CardWallController { + val cardWallUseCase by rememberInstance() + val authenticationUseCase by rememberInstance() + val dispatchers by rememberInstance() + return remember { + CardWallController( + cardWallUseCase = cardWallUseCase, + authenticationUseCase = authenticationUseCase, + dispatchers = dispatchers + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt index d49da4a3..72aff0bd 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt @@ -114,7 +114,7 @@ sealed class CardWallAuthenticationData( fun CardWallNfcInstructionScreen( onClickTroubleshooting: () -> Unit, onBack: () -> Unit, - viewModel: CardWallViewModel, + cardWallController: CardWallController, authenticationData: CardWallAuthenticationData, profileId: ProfileIdentifier, onNext: () -> Unit, @@ -127,14 +127,14 @@ fun CardWallNfcInstructionScreen( val dialogState = rememberCardWallAuthenticationDialogState() - if (!viewModel.isNFCEnabled()) { + if (!cardWallController.isNFCEnabled()) { EnableNfcDialog { onBack() } } else { CardWallAuthenticationDialog( dialogState = dialogState, - viewModel = viewModel, + cardWallController = cardWallController, authenticationData = authenticationData, profileId = profileId, troubleShootingEnabled = true, diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallTroubleshooting.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallTroubleshooting.kt deleted file mode 100644 index 8c0e60cd..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallTroubleshooting.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.cardwall.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.launch - -@Composable -fun CardWallTroubleshootingPageA( - viewModel: CardWallViewModel, - authenticationData: CardWallAuthenticationData, - profileId: ProfileIdentifier, - onFinal: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onRetryCan: () -> Unit, - onRetryPin: () -> Unit, - onUnlockEgk: () -> Unit -) { - val dialogState = rememberCardWallAuthenticationDialogState() - - CardWallAuthenticationDialog( - profileId = profileId, - dialogState = dialogState, - viewModel = viewModel, - authenticationData = authenticationData, - onFinal = onFinal, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageAContent( - onBack = onBack, - onNext = onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Composable -fun CardWallTroubleshootingPageB( - viewModel: CardWallViewModel, - authenticationData: CardWallAuthenticationData, - profileId: ProfileIdentifier, - onFinal: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onRetryCan: () -> Unit, - onRetryPin: () -> Unit, - onUnlockEgk: () -> Unit -) { - val dialogState = rememberCardWallAuthenticationDialogState() - - CardWallAuthenticationDialog( - dialogState = dialogState, - viewModel = viewModel, - authenticationData = authenticationData, - profileId = profileId, - onFinal = onFinal, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageBContent( - onBack, - onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Composable -fun CardWallTroubleshootingPageC( - viewModel: CardWallViewModel, - authenticationData: CardWallAuthenticationData, - profileId: ProfileIdentifier, - onFinal: () -> Unit, - onNext: () -> Unit, - onBack: () -> Unit, - onRetryCan: () -> Unit, - onRetryPin: () -> Unit, - onUnlockEgk: () -> Unit -) { - val dialogState = rememberCardWallAuthenticationDialogState() - - CardWallAuthenticationDialog( - profileId = profileId, - dialogState = dialogState, - viewModel = viewModel, - authenticationData = authenticationData, - onFinal = onFinal, - onRetryCan = onRetryCan, - onRetryPin = onRetryPin, - onUnlockEgk = onUnlockEgk - ) - - val coroutineScope = rememberCoroutineScope() - TroubleshootingPageCContent( - onBack, - onNext, - onClickTryMe = { - coroutineScope.launch { dialogState.show() } - } - ) -} - -@Composable -fun CardWallTroubleshootingNoSuccessPage( - onNext: () -> Unit, - onBack: () -> Unit -) { - TroubleshootingNoSuccessPageContent( - onNext, - onBack - ) -} 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 4ec7e8e1..c467c89c 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,21 +21,17 @@ package de.gematik.ti.erp.app.cardwall.ui.model import de.gematik.ti.erp.app.Route object CardWallNavigation { - object TroubleshootingPageA : Route("TroubleshootingPageA") - object TroubleshootingPageB : Route("TroubleshootingPageB") - object TroubleshootingPageC : Route("TroubleshootingPageC") - object TroubleshootingNoSuccessPage : Route("TroubleshootingNoSuccessPage") - object ExternalAuthenticator : Route("ExternalAuthenticatorOverview") - object Intro : Route("CardWallIntro") - object MissingCapabilities : Route("MissingCapabilities") - object CardAccessNumber : Route("CardWallCardAccessNumber") + 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("CardWallPersonalIdentificationNumber") - object AuthenticationSelection : Route("CardWallAuthenticationSelection") - object AlternativeOption : Route("AlternativeOption") + 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("CardWallAuthentication") - object InsuranceApp : Route("InsuranceApp") - object OrderHealthCard : Route("OrderHealthCard") - object UnlockEgk : Route("UnlockEgk") + object Authentication : Route("card_wall_authentication") + object OrderHealthCard : Route("card_wall_order_health_card") + object UnlockEgk : Route("card_wall_unlock_egk") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt b/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt index f3406cdc..8ca8c208 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt @@ -18,6 +18,10 @@ package de.gematik.ti.erp.app.core +import androidx.compose.runtime.saveable.Saver +import de.gematik.ti.erp.app.App +import java.util.UUID + class AppScopedCache { private val data: MutableMap = mutableMapOf() private val lock = Any() @@ -33,3 +37,21 @@ class AppScopedCache { data.remove(key) } } + +fun complexAutoSaver(): Saver = complexAutoSaver(init = {}) + +fun complexAutoSaver( + init: T.() -> Unit +): Saver = Saver( + save = { state -> + val key = UUID.randomUUID().toString() + App.cache.store(key, state) + key + }, + restore = { key -> + @Suppress("UNCHECKED_CAST") + (App.cache.recover(key) as T).apply { + init() + } + } +) diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt index 3ad46a9f..47fdf9d6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt @@ -18,6 +18,8 @@ package de.gematik.ti.erp.app.core +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.gematik.ti.erp.app.attestation.usecase.IntegrityUseCase @@ -51,8 +53,6 @@ class MainViewModel( } } - var showDataTermsUpdate = settingsUseCase.showDataTermsUpdate - var integrityPromptShown = false fun checkDeviceIntegrity() = integrityUseCase.runIntegrityAttestation().map { @@ -63,6 +63,7 @@ class MainViewModel( true } } + fun onAcceptInsecureDevice() { viewModelScope.launch { settingsUseCase.acceptInsecureDevice() @@ -92,9 +93,14 @@ class MainViewModel( fun showMainScreenToolTips(): Flow = settingsUseCase.general .map { !it.mainScreenTooltipsShown && it.welcomeDrawerShown } - fun dataProtectionVersionAcceptedOn() = - settingsUseCase.general.map { it.dataProtectionVersionAcceptedOn } - fun mlKitNotAccepted() = settingsUseCase.general.map { !it.mlKitAccepted } + + fun talkbackEnabled(context: Context): Boolean { + val accessibilityManager = + context.getSystemService(Context.ACCESSIBILITY_SERVICE) as android.view.accessibility.AccessibilityManager + + return accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN) + .isNotEmpty() + } } 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 4a16c96f..7fdebc27 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 @@ -79,7 +79,7 @@ val allModules = DI.Module("allModules") { bindSingleton { FeatureToggleManager(instance()) } - bindSingleton { Analytics(instance()) } + bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag)) } importAll( cardWallModule, diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt b/android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt index 3e7e9095..4befd78f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt @@ -24,7 +24,7 @@ import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -import de.gematik.ti.erp.app.BuildConfig +import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.MessageConversionException import de.gematik.ti.erp.app.db.appSchemas import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 @@ -70,8 +70,8 @@ val realmModule = DI.Module("realmModule") { ).also { realm -> realm.writeBlocking { queryFirst()?.let { - it.latestAppVersionName = BuildConfig.VERSION_NAME - it.latestAppVersionCode = BuildConfig.VERSION_CODE + it.latestAppVersionName = BuildKonfig.VERSION_NAME + it.latestAppVersionCode = BuildKonfig.VERSION_CODE } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt b/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt index 610b89ad..a3bc31e8 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt @@ -28,7 +28,8 @@ import kotlinx.coroutines.flow.map private val Context.dataStore by preferencesDataStore("featureToggles") enum class Features(val featureName: String) { - REDEEM_WITHOUT_TI("RedeemWithoutTI") + REDEEM_WITHOUT_TI("RedeemWithoutTI"), + PKV("PKV") } class FeatureToggleManager(val context: Context) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataTermsUpdateScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataTermsUpdateScreen.kt deleted file mode 100644 index 339b52d1..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataTermsUpdateScreen.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.mainscreen.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.foundation.layout.statusBarsPadding -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.settings.usecase.DATA_PROTECTION_LAST_UPDATED -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.BottomAppBar -import de.gematik.ti.erp.app.utils.compose.Spacer16 -import de.gematik.ti.erp.app.utils.compose.Spacer24 -import de.gematik.ti.erp.app.utils.compose.Spacer48 -import de.gematik.ti.erp.app.utils.compose.Spacer8 -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import java.time.Instant -import java.time.OffsetDateTime -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -@Composable -fun DataTermsUpdateScreen( - dataProtectionVersionAcceptedOn: Instant, - onClickDataTerms: () -> Unit, - onAcceptTermsOfUseUpdate: () -> Unit -) { - Scaffold( - bottomBar = { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(modifier = Modifier.weight(1f)) - Button( - onClick = { - onAcceptTermsOfUseUpdate() - }, - shape = RoundedCornerShape(PaddingDefaults.Small), - modifier = Modifier.padding(end = PaddingDefaults.Small) - ) { - Text(stringResource(id = R.string.data_terms_accept_update)) - } - } - } - ) { innerPadding -> - Column( - modifier = Modifier.padding(innerPadding) - .verticalScroll(rememberScrollState()) - .statusBarsPadding() - ) { - Column( - modifier = Modifier - .padding(PaddingDefaults.Medium) - ) { - Text( - stringResource(R.string.data_terms_update_header), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - - Spacer24() - Text( - stringResource(R.string.data_terms_update_info), - style = AppTheme.typography.body1 - ) - - Spacer8() - TextButton( - onClick = { onClickDataTerms() }, - modifier = Modifier.align(Alignment.End) - ) { - Text( - stringResource(R.string.data_terms_update_open_data_terms), - style = AppTheme.typography.caption1 - ) - } - - val dtFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } - val date = remember(dataProtectionVersionAcceptedOn) { - OffsetDateTime.ofInstant(dataProtectionVersionAcceptedOn, ZoneId.systemDefault()) - .toLocalDate() - .format(dtFormatter) - } - - val updateInfo = annotatedStringResource( - R.string.data_terms_update_updates, - date - ).toString() - Spacer48() - Text( - updateInfo, - style = AppTheme.typography.subtitle1 - ) - Spacer16() - } - Column(modifier = Modifier.fillMaxWidth()) { - if (dataProtectionVersionAcceptedOn < DATA_PROTECTION_LAST_UPDATED) { - DPDifferences30112021() - } - } - } - } -} 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 6355778f..a4e971ed 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 @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.mainscreen.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding @@ -37,6 +38,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MarkChatRead @@ -63,6 +65,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -87,7 +90,6 @@ 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.MainViewModel import de.gematik.ti.erp.app.debug.ui.DebugScreenWrapper -import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.license.ui.LicenseScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingNavigationScreens import de.gematik.ti.erp.app.onboarding.ui.OnboardingScreen @@ -105,7 +107,6 @@ import de.gematik.ti.erp.app.prescription.ui.PrescriptionScreen import de.gematik.ti.erp.app.prescription.ui.PrescriptionViewModel import de.gematik.ti.erp.app.prescription.ui.ScanPrescriptionViewModel import de.gematik.ti.erp.app.prescription.ui.ScanScreen -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.ui.DefaultProfile import de.gematik.ti.erp.app.profiles.ui.EditProfileScreen import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler @@ -138,7 +139,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.kodein.di.compose.rememberViewModel -import java.time.Instant @Suppress("LongMethod") @Composable @@ -149,33 +149,9 @@ fun MainScreen( settingsViewModel: SettingsViewModel, profileSettingsViewModel: ProfileSettingsViewModel ) { - LaunchedEffect(Unit) { - mainViewModel.authenticationMethod.collect { - if (!mainViewModel.showOnboarding && !( - it is SettingsData.AuthenticationMode.Password || - it == SettingsData.AuthenticationMode.DeviceSecurity - ) - ) { - navController.navigate(MainNavigationScreens.ReturningUserSecureAppOnboarding.path()) { - launchSingleTop = true - popUpTo(MainNavigationScreens.Prescriptions.path()) { - inclusive = true - } - } - } - } - } + CheckAuthenticationMethod(mainViewModel, navController) - val startDestination = - when { - mainViewModel.showOnboarding -> { - MainNavigationScreens.Onboarding.route - } - - else -> { - MainNavigationScreens.Prescriptions.route - } - } + val startDestination = determineStartDestination(mainViewModel) TrackNavigationChanges(navController) val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) @@ -207,21 +183,6 @@ fun MainScreen( ) } } - composable(MainNavigationScreens.DataTermsUpdateScreen.route) { - val dataProtectionVersionAccepted: Instant? by mainViewModel - .dataProtectionVersionAcceptedOn() - .collectAsState(initial = null) - - dataProtectionVersionAccepted?.let { acceptedOn -> - DataTermsUpdateScreen( - acceptedOn, - onClickDataTerms = { navController.navigate(MainNavigationScreens.DataProtection.route) } - ) { - mainViewModel.acceptUpdatedDataTerms() - navController.navigate(MainNavigationScreens.Prescriptions.route) - } - } - } composable(MainNavigationScreens.DataProtection.route) { NavigationAnimation(mode = navigationMode) { WebViewScreen( @@ -495,7 +456,7 @@ fun MainScreen( UnlockMethod.ResetRetryCounterWithNewSecret.name -> UnlockMethod.ResetRetryCounterWithNewSecret else -> UnlockMethod.None }, - navController = navController, + onCancel = { navController.popBackStack() }, onClickLearnMore = { navController.navigate( MainNavigationScreens.OrderHealthCard.path() @@ -519,17 +480,48 @@ fun MainScreen( } } -@Suppress("LongMethod", "ComplexMethod") +@Composable +private fun determineStartDestination(mainViewModel: MainViewModel) = + when { + mainViewModel.showOnboarding -> { + MainNavigationScreens.Onboarding.route + } + + else -> { + MainNavigationScreens.Prescriptions.route + } + } + +@Composable +private fun CheckAuthenticationMethod(mainViewModel: MainViewModel, navController: NavHostController) { + LaunchedEffect(Unit) { + mainViewModel.authenticationMethod.collect { + if (!mainViewModel.showOnboarding && !( + it is SettingsData.AuthenticationMode.Password || + it == SettingsData.AuthenticationMode.DeviceSecurity + ) + ) { + navController.navigate(MainNavigationScreens.ReturningUserSecureAppOnboarding.path()) { + launchSingleTop = true + popUpTo(MainNavigationScreens.Prescriptions.path()) { + inclusive = true + } + } + } + } + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable -fun MainScreenWithScaffold( +private fun MainScreenWithScaffold( mainNavController: NavController, mainViewModel: MainViewModel, mainScreenViewModel: MainScreenViewModel, settingsViewModel: SettingsViewModel, profileSettingsViewModel: ProfileSettingsViewModel ) { - val profileHandler = LocalProfileHandler.current + val context = LocalContext.current val bottomNavController = rememberNavController() val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsState(null) @@ -540,34 +532,8 @@ fun MainScreenWithScaffold( } } - LaunchedEffect(Unit) { - withContext(Dispatchers.Main) { - if (mainViewModel.showDataTermsUpdate.first()) { - mainNavController.navigate( - MainNavigationScreens.DataTermsUpdateScreen.path(), - navOptions { - launchSingleTop = true - popUpTo(MainNavigationScreens.Prescriptions.path()) { - inclusive = true - } - } - ) - } else if (mainViewModel.showInsecureDevicePrompt.first()) { - mainNavController.navigate(MainNavigationScreens.InsecureDeviceScreen.path()) - } - } - } - - LaunchedEffect(Unit) { - if (BuildConfig.DEBUG) { - return@LaunchedEffect - } - if (!mainViewModel.checkDeviceIntegrity().first()) { - withContext(Dispatchers.Main) { - mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) - } - } - } + CheckInsecureDevice(mainViewModel, mainNavController) + CheckDeviceIntegrity(mainViewModel, mainNavController) val scaffoldState = rememberScaffoldState() @@ -586,6 +552,7 @@ fun MainScreenWithScaffold( it != ModalBottomSheetValue.HalfExpanded } ) + LaunchedEffect(Unit) { sheetState.snapTo(ModalBottomSheetValue.Hidden) } @@ -613,6 +580,12 @@ fun MainScreenWithScaffold( } } + LaunchedEffect(Unit) { + if (mainViewModel.talkbackEnabled(context)) { + mainViewModel.mainScreenTooltipsShown() + } + } + var profileToRename by remember { mutableStateOf(DefaultProfile) } @@ -656,90 +629,184 @@ fun MainScreenWithScaffold( // TODO: move to general place? ExternalAuthenticationDialog() - var topBarElevated by remember { mutableStateOf(true) } - - Scaffold( - modifier = Modifier.testTag(TestTag.Main.MainScreen), - topBar = { - if (currentBottomNavigationRoute?.destination?.route != MainNavigationScreens.Settings.route) { - MultiProfileTopAppBar( - navController = mainNavController, - elevated = topBarElevated, - mainScreenViewModel = mainScreenViewModel, - mainViewModel = mainViewModel, - isInPrescriptionScreen = isInPrescriptionScreen, - onClickAddProfile = { - mainScreenBottomSheetContentState = - MainScreenBottomSheetContentState.EditOrAddProfileName(addProfile = true) - }, - onClickChangeProfileName = { profile -> - profileToRename = profile - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditOrAddProfileName() - }, - tooltipBounds = toolTipBounds - ) - } + MainScreenScaffold( + mainViewModel = mainViewModel, + mainScreenViewModel = mainScreenViewModel, + settingsViewModel = settingsViewModel, + mainNavController = mainNavController, + bottomNavController = bottomNavController, + tooltipBounds = toolTipBounds, + + onClickAddProfile = { + mainScreenBottomSheetContentState = + MainScreenBottomSheetContentState.EditOrAddProfileName(addProfile = true) }, - bottomBar = { - MainScreenBottomNavigation( - navController = mainNavController, - viewModel = mainScreenViewModel, - bottomNavController = bottomNavController, - profileId = profileHandler.activeProfile.id - ) + onClickChangeProfileName = { profile -> + profileToRename = profile + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditOrAddProfileName() }, - floatingActionButton = { - if (isInPrescriptionScreen) { - RedeemFloatingActionButton( - onClick = { - mainNavController.navigate(MainNavigationScreens.Redeem.path()) - } - ) - } + + onClickAvatar = { + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfile }, scaffoldState = scaffoldState - ) { innerPadding -> - Box( - modifier = Modifier - .padding(innerPadding) - .testTag("main_screen") - ) { - NavHost( - bottomNavController, - startDestination = MainNavigationScreens.Prescriptions.path() - ) { - composable(MainNavigationScreens.Prescriptions.route) { - val prescriptionViewModel by rememberViewModel() - PrescriptionScreen( - navController = mainNavController, - onClickAvatar = { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfile - }, - prescriptionViewModel = prescriptionViewModel, - mainScreenViewModel = mainScreenViewModel, - onElevateTopBar = { - topBarElevated = it - }, - onClickArchive = { mainNavController.navigate(MainNavigationScreens.Archive.path()) } - ) + ) + } +} + +@Composable +private fun MainScreenScaffold( + mainViewModel: MainViewModel, + mainScreenViewModel: MainScreenViewModel, + settingsViewModel: SettingsViewModel, + mainNavController: NavController, + bottomNavController: NavHostController, + tooltipBounds: MutableState>, + onClickAddProfile: () -> Unit, + onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, + onClickAvatar: () -> Unit, + scaffoldState: ScaffoldState +) { + val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsState(null) + + val isInPrescriptionScreen by remember { + derivedStateOf { + currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route + } + } + var topBarElevated by remember { mutableStateOf(true) } + + Scaffold( + modifier = Modifier.testTag(TestTag.Main.MainScreen), + topBar = { + if (currentBottomNavigationRoute?.destination?.route != MainNavigationScreens.Settings.route) { + MultiProfileTopAppBar( + navController = mainNavController, + elevated = topBarElevated, + mainScreenViewModel = mainScreenViewModel, + mainViewModel = mainViewModel, + isInPrescriptionScreen = isInPrescriptionScreen, + onClickAddProfile = onClickAddProfile, + onClickChangeProfileName = onClickChangeProfileName, + tooltipBounds = tooltipBounds + ) + } + }, + bottomBar = { + MainScreenBottomBar( + navController = mainNavController, + viewModel = mainScreenViewModel, + bottomNavController = bottomNavController + ) + }, + floatingActionButton = { + if (isInPrescriptionScreen) { + RedeemFloatingActionButton( + onClick = { + mainNavController.navigate(MainNavigationScreens.Redeem.path()) } - composable(MainNavigationScreens.Orders.route) { - OrderScreen( - mainNavController = mainNavController, - mainScreenViewModel = mainScreenViewModel, - onElevateTopBar = { - topBarElevated = it - } - ) + ) + } + }, + scaffoldState = scaffoldState + ) { innerPadding -> + + MainScreenBottomNavHost( + mainScreenViewModel = mainScreenViewModel, + settingsViewModel = settingsViewModel, + mainNavController = mainNavController, + bottomNavController = bottomNavController, + innerPadding = innerPadding, + onClickAvatar = onClickAvatar, + onElevateTopBar = { + topBarElevated = it + }, + onClickArchive = { mainNavController.navigate(MainNavigationScreens.Archive.path()) } + ) + } +} + +@Composable +private fun MainScreenBottomNavHost( + mainScreenViewModel: MainScreenViewModel, + settingsViewModel: SettingsViewModel, + mainNavController: NavController, + bottomNavController: NavHostController, + innerPadding: PaddingValues, + onClickAvatar: () -> Unit, + onElevateTopBar: (Boolean) -> Unit, + onClickArchive: () -> Unit +) { + Box( + modifier = Modifier + .padding(innerPadding) + .testTag("main_screen") + ) { + NavHost( + bottomNavController, + startDestination = MainNavigationScreens.Prescriptions.path() + ) { + composable(MainNavigationScreens.Prescriptions.route) { + val prescriptionViewModel by rememberViewModel() + PrescriptionScreen( + navController = mainNavController, + onClickAvatar = onClickAvatar, + prescriptionViewModel = prescriptionViewModel, + mainScreenViewModel = mainScreenViewModel, + onElevateTopBar = onElevateTopBar, + onClickArchive = onClickArchive + ) + } + composable(MainNavigationScreens.Orders.route) { + OrderScreen( + mainNavController = mainNavController, + mainScreenViewModel = mainScreenViewModel, + onElevateTopBar = onElevateTopBar + ) + } + composable( + MainNavigationScreens.Settings.route, + MainNavigationScreens.Settings.arguments + ) { + SettingsScreen( + mainNavController = mainNavController, + settingsViewModel = settingsViewModel + ) + } + } + } +} + +@Composable +private fun CheckDeviceIntegrity(mainViewModel: MainViewModel, mainNavController: NavController) { + LaunchedEffect(Unit) { + if (BuildConfig.DEBUG) { + return@LaunchedEffect + } + if (!mainViewModel.checkDeviceIntegrity().first()) { + withContext(Dispatchers.Main) { + mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) + navOptions { + launchSingleTop = true + popUpTo(MainNavigationScreens.Prescriptions.path()) { + inclusive = true } - composable( - MainNavigationScreens.Settings.route, - MainNavigationScreens.Settings.arguments - ) { - SettingsScreen( - mainNavController = mainNavController, - settingsViewModel = settingsViewModel - ) + } + } + } + } +} + +@Composable +private fun CheckInsecureDevice(mainViewModel: MainViewModel, mainNavController: NavController) { + LaunchedEffect(Unit) { + withContext(Dispatchers.Main) { + if (mainViewModel.showInsecureDevicePrompt.first()) { + mainNavController.navigate(MainNavigationScreens.InsecureDeviceScreen.path()) + navOptions { + launchSingleTop = true + popUpTo(MainNavigationScreens.Prescriptions.path()) { + inclusive = true } } } @@ -748,14 +815,15 @@ fun MainScreenWithScaffold( } @Composable -fun MainScreenBottomNavigation( +private fun MainScreenBottomBar( navController: NavController, bottomNavController: NavController, - viewModel: MainScreenViewModel, - profileId: ProfileIdentifier + viewModel: MainScreenViewModel ) { val navBackStackEntry by bottomNavController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route + val profileHandler = LocalProfileHandler.current + val profileId = profileHandler.activeProfile.id val unreadMessagesAvailable by viewModel.unreadMessagesAvailable(profileId) .collectAsState(initial = false) @@ -835,7 +903,7 @@ fun MainScreenBottomNavigation( } @Composable -fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { +private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { val text = if (isInPrescriptionScreen) { stringResource(R.string.pres_bottombar_prescriptions) } else { @@ -850,7 +918,7 @@ fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { } @Composable -fun ProfilesChipBar( +private fun ProfilesChipBar( mainScreenViewModel: MainScreenViewModel, onClickAddProfile: () -> Unit, onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, @@ -909,19 +977,11 @@ fun ProfilesChipBar( } } -@Composable -fun ssoStatusColor(profile: ProfilesUseCaseData.Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = - when { - ssoTokenScope?.token?.isValid() == true -> AppTheme.colors.green400 - profile.lastAuthenticated != null -> AppTheme.colors.neutral400 - else -> null - } - /** * The top appbar of the actual main screen. */ @Composable -fun MultiProfileTopAppBar( +private fun MultiProfileTopAppBar( navController: NavController, mainScreenViewModel: MainScreenViewModel, mainViewModel: MainViewModel, diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt index 3d211a48..11eae86b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt @@ -65,6 +65,7 @@ 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.idp.model.IdpData import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.ui.rememberRefreshPrescriptionsController @@ -252,3 +253,11 @@ fun ProfileChip( } } } + +@Composable +private fun ssoStatusColor(profile: ProfilesUseCaseData.Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = + when { + ssoTokenScope?.token?.isValid() == true -> AppTheme.colors.green400 + profile.lastAuthenticated != null -> AppTheme.colors.neutral400 + else -> null + } diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt index 6e25a622..5990ef1e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.offset import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.core.MainViewModel import de.gematik.ti.erp.app.theme.AppTheme 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 a281792a..36c2ba49 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 @@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.toggleable @@ -68,7 +67,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -299,8 +297,6 @@ fun OnboardingScreen( } } -@Suppress("LongMethod") -@OptIn(ExperimentalAnimationApi::class) @Composable private fun OnboardingScreenWithScaffold( navController: NavController, @@ -314,8 +310,6 @@ private fun OnboardingScreenWithScaffold( secureAppMethod: OnboardingSecureAppMethod ) -> Unit ) { - val context = LocalContext.current - val defaultProfileName = stringResource(R.string.onboarding_default_profile_name) Box { @@ -331,90 +325,108 @@ private fun OnboardingScreenWithScaffold( page = OnboardingPages.pageOf(page.index - 1) } - AnimatedContent( - modifier = Modifier.fillMaxSize(), - targetState = page, - transitionSpec = { - when { - initialState == OnboardingPages.Welcome && - targetState == OnboardingPages.pageOf(1) -> { - fadeIn(tween(durationMillis = 770)) with fadeOut(tween(durationMillis = 770)) - } + OnboardingPages( + page, + navController, + defaultProfileName, + secureMethod, + onSaveNewUser, + allowTracking, + onAllowTracking, + onSecureMethodChange + ) { + page = it + } - initialState.index > targetState.index -> { - slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with - slideOutOfContainer(AnimatedContentScope.SlideDirection.Right) - } + if (BuildKonfig.INTERNAL) { + SkipOnBoardingButton(onClick = { + onSaveNewUser( + false, + defaultProfileName, + OnboardingSecureAppMethod.Password("a", "a", 9) + ) + }) + } + } +} - else -> { - slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with - slideOutOfContainer(AnimatedContentScope.SlideDirection.Left) - } - } - } - ) { - when (it) { - OnboardingPages.Welcome -> { - OnboardingWelcome( - onNextPage = { - page = OnboardingPages.DataProtection - } - ) - } +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun OnboardingPages( + page: OnboardingPages, + navController: NavController, + defaultProfileName: String, + secureMethod: OnboardingSecureAppMethod, + onSaveNewUser: ( + allowTracking: Boolean, + defaultProfileName: String, + secureAppMethod: OnboardingSecureAppMethod + ) -> Unit, + allowTracking: Boolean, + onAllowTracking: (Boolean) -> Unit, + onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit, + onNextPage: (OnboardingPages) -> Unit +) { + val context = LocalContext.current - OnboardingPages.DataProtection -> { - OnboardingPageTerms( - navController = navController, - onNextPage = { - page = OnboardingPages.SecureApp - } - ) + AnimatedContent( + modifier = Modifier.fillMaxSize(), + targetState = page, + transitionSpec = { + when { + initialState == OnboardingPages.Welcome && + targetState == OnboardingPages.pageOf(1) -> { + fadeIn(tween(durationMillis = 770)) with fadeOut(tween(durationMillis = 770)) } - OnboardingPages.SecureApp -> { - OnboardingSecureApp( - secureMethod = secureMethod, - onSecureMethodChange = onSecureMethodChange, - onOpenBiometricScreen = { - navController.navigate(OnboardingNavigationScreens.Biometry.path()) - }, - onNextPage = { - page = OnboardingPages.Analytics - } - ) + initialState.index > targetState.index -> { + slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with + slideOutOfContainer(AnimatedContentScope.SlideDirection.Right) } - OnboardingPages.Analytics -> { - val disAllowToast = stringResource(R.string.settings_tracking_disallow_info) - OnboardingPageAnalytics( - allowAnalytics = allowTracking, - onAllowAnalytics = { - if (!it) { - onAllowTracking(false) - createToastShort(context, disAllowToast) - } else { - navController.navigate(OnboardingNavigationScreens.Analytics.path()) - } - }, - onNextPage = { - onSaveNewUser(allowTracking, defaultProfileName, secureMethod) - } - ) + else -> { + slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with + slideOutOfContainer(AnimatedContentScope.SlideDirection.Left) } } } + ) { + when (it) { + OnboardingPages.Welcome -> { + OnboardingWelcome { onNextPage(OnboardingPages.DataProtection) } + } - if (BuildKonfig.INTERNAL) { - Row( - modifier = Modifier - .align(Alignment.TopEnd) - .systemBarsPadding() - .padding(PaddingDefaults.Medium) - ) { - OutlinedDebugButton( - "SKIP", - onClick = { - onSaveNewUser(false, defaultProfileName, OnboardingSecureAppMethod.Password("a", "a", 9)) + OnboardingPages.DataProtection -> { + OnboardingPageTerms( + navController = navController + ) { onNextPage(OnboardingPages.SecureApp) } + } + + OnboardingPages.SecureApp -> { + OnboardingSecureApp( + secureMethod = secureMethod, + onSecureMethodChange = onSecureMethodChange, + onOpenBiometricScreen = { + navController.navigate(OnboardingNavigationScreens.Biometry.path()) + }, + onNextPage = { onNextPage(OnboardingPages.Analytics) } + ) + } + + OnboardingPages.Analytics -> { + val disAllowToast = stringResource(R.string.settings_tracking_disallow_info) + OnboardingPageAnalytics( + allowAnalytics = allowTracking, + onAllowAnalytics = { allow -> + if (!allow) { + onAllowTracking(false) + createToastShort(context, disAllowToast) + } else { + navController.navigate(OnboardingNavigationScreens.Analytics.path()) + } + }, + onNextPage = { + onSaveNewUser(allowTracking, defaultProfileName, secureMethod) } ) } @@ -422,6 +434,23 @@ private fun OnboardingScreenWithScaffold( } } +@Composable +fun SkipOnBoardingButton(onClick: () -> Unit) { + Box(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .align(Alignment.TopEnd) + .systemBarsPadding() + .padding(PaddingDefaults.Medium) + ) { + OutlinedDebugButton( + "SKIP", + onClick = onClick + ) + } + } +} + @Composable private fun OnboardingWelcome( onNextPage: () -> Unit diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt b/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt index 53a581e5..2f89d723 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt @@ -34,7 +34,7 @@ import de.gematik.ti.erp.app.prescription.repository.RemoteDataSource import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext -import java.time.Instant +import kotlinx.datetime.Instant private const val CommunicationsMaxPageSize = 50 @@ -70,6 +70,8 @@ class CommunicationRepository( } } + override val tag: String = "CommunicationRepository" + suspend fun downloadCommunications(profileId: ProfileIdentifier) = downloadPaged(profileId) override suspend fun downloadResource( diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt index fb124ba2..65975551 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt @@ -42,11 +42,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.redeem.ui.createBitMatrix import de.gematik.ti.erp.app.redeem.ui.drawDataMatrix @@ -56,8 +60,9 @@ import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import java.time.LocalDateTime -import java.time.ZoneId +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -94,6 +99,7 @@ fun MessageSheetContent( Modifier .fillMaxWidth() .verticalScroll(rememberScrollState()) + .testTag(TestTag.Orders.Messages.Content) ) { when (message.type) { OrderUseCaseData.Message.Type.All -> @@ -148,7 +154,9 @@ fun LinkSheetContent( val uriHandler = LocalUriHandler.current Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Orders.Messages.Link), horizontalAlignment = Alignment.CenterHorizontally ) { if (message.type != OrderUseCaseData.Message.Type.All) { @@ -166,6 +174,7 @@ fun LinkSheetContent( SpacerLarge() } PrimaryButtonSmall( + modifier = Modifier.testTag(TestTag.Orders.Messages.LinkButton), onClick = { message.link?.let { uriHandler.openUri(it) } } @@ -179,7 +188,11 @@ fun LinkSheetContent( fun TextSheetContent( message: OrderUseCaseData.Message ) { - Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .semantics(true) { testTag = TestTag.Orders.Messages.Text } + ) { Box( Modifier .fillMaxWidth() @@ -205,7 +218,7 @@ fun TextSheetContent( private fun sentOn(message: OrderUseCaseData.Message): String = remember(message) { val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) - dateFormatter.format(LocalDateTime.ofInstant(message.sentOn, ZoneId.systemDefault())) + dateFormatter.format(message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } @Composable @@ -244,6 +257,7 @@ private fun CodeLabel( Modifier .background(AppTheme.colors.neutral100, RoundedCornerShape(8.dp)) .padding(horizontal = PaddingDefaults.ShortMedium, vertical = PaddingDefaults.ShortMedium / 2) + .semantics(true) { testTag = TestTag.Orders.Messages.CodeLabelContent } ) { Text( code, @@ -257,7 +271,12 @@ private fun CodeLabel( @Composable fun EmptySheetContent(pharmacyName: String) { - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Column( + Modifier + .fillMaxWidth() + .testTag(TestTag.Orders.Messages.Empty), + horizontalAlignment = Alignment.CenterHorizontally + ) { Text( stringResource(R.string.orders_no_message_title), style = AppTheme.typography.subtitle1, @@ -281,6 +300,7 @@ private fun DataMatrixCode(payload: String, modifier: Modifier) { .then(modifier) .background(Color.White) .padding(PaddingDefaults.Small) + .testTag(TestTag.Orders.Messages.Code) ) { Box( modifier = Modifier 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 b9dbe69e..d1d41f3d 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 @@ -79,6 +79,7 @@ import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em @@ -107,6 +108,7 @@ import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.timeDescription import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.first @@ -114,9 +116,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import org.kodein.di.compose.rememberInstance -import java.time.LocalDateTime -import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -200,7 +203,7 @@ fun MessageScreen( listState = listState, navigationMode = NavigationBarMode.Back, onBack = { - scope.launch { + scope.launch(Dispatchers.Main) { state.consumeAllMessages() mainNavController.popBackStack() } @@ -565,6 +568,8 @@ private fun Messages( Text( med, style = AppTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) SpacerMedium() @@ -601,11 +606,11 @@ private fun ReplyMessage( val date = remember(message) { val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - dateFormatter.format(LocalDateTime.ofInstant(message.sentOn, ZoneId.systemDefault())) + dateFormatter.format(message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } val time = remember(message) { val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - dateFormatter.format(LocalDateTime.ofInstant(message.sentOn, ZoneId.systemDefault())) + dateFormatter.format(message.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } Column( @@ -619,6 +624,7 @@ private fun ReplyMessage( enabled = message.type != OrderUseCaseData.Message.Type.Text ) .fillMaxWidth() + .testTag(TestTag.Orders.Details.MessageListItem) ) { Row { Spacer(Modifier.width(48.dp)) @@ -680,11 +686,11 @@ private fun DispenseMessage( ) { val date = remember(order) { val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) - dateFormatter.format(LocalDateTime.ofInstant(order.sentOn, ZoneId.systemDefault())) + dateFormatter.format(order.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } val time = remember(order) { val dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) - dateFormatter.format(LocalDateTime.ofInstant(order.sentOn, ZoneId.systemDefault())) + dateFormatter.format(order.sentOn.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime()) } Row( Modifier.drawConnectedLine( diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt index 41b5ed27..0195d8b0 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt @@ -18,7 +18,7 @@ package de.gematik.ti.erp.app.orders.usecase.model -import java.time.Instant +import kotlinx.datetime.Instant object OrderUseCaseData { data class Pharmacy( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt index 00b88792..3a2c9588 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/model/PharmacyData.kt @@ -19,7 +19,7 @@ package de.gematik.ti.erp.app.pharmacy.model import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import java.time.Instant +import kotlinx.datetime.Instant object PharmacyData { data class ShippingContact( 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 fa642752..735f9eb9 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 @@ -31,7 +31,7 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.query.Sort import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import java.time.Instant +import kotlinx.datetime.Clock import javax.inject.Inject class PharmacyLocalDataSource @Inject constructor( @@ -58,7 +58,7 @@ class PharmacyLocalDataSource @Inject constructor( suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { realm.tryWrite { queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Instant.now().toRealmInstant() + this.lastUsed = Clock.System.now().toRealmInstant() this.usageCount += 1 } ?: copyToRealm(pharmacy.toOftenUsedPharmacyEntityV1()) } @@ -97,7 +97,7 @@ class PharmacyLocalDataSource @Inject constructor( suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { realm.tryWrite { queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Instant.now().toRealmInstant() + this.lastUsed = Clock.System.now().toRealmInstant() } ?: copyToRealm(pharmacy.toFavoritePharmacyEntityV1()) } } 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 c737a22f..8d6cbacf 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 @@ -104,7 +104,10 @@ import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.provideEmailIntent import de.gematik.ti.erp.app.utils.compose.providePhoneIntent import kotlinx.coroutines.launch -import java.time.OffsetDateTime +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.format.TextStyle @@ -423,7 +426,7 @@ private fun PharmacyOpeningHours(openingHours: OpeningHours) { for (h in sortedOpeningHours) { val (day, hours) = h - val now = remember { OffsetDateTime.now() } + val now = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } val isOpenToday = remember(now) { h.isOpenToday(now) } Row( modifier = Modifier.fillMaxWidth(), @@ -437,11 +440,11 @@ private fun PharmacyOpeningHours(openingHours: OpeningHours) { horizontalAlignment = Alignment.End ) { for (hour in hours.sortedBy { it.openingTime }) { - val opens = hour.openingTime.format(dateTimeFormatter) - val closes = hour.closingTime.format(dateTimeFormatter) + val opens = hour.openingTime?.toJavaLocalTime()?.format(dateTimeFormatter) ?: "" + val closes = hour.closingTime?.toJavaLocalTime()?.format(dateTimeFormatter) ?: "" val text = "$opens - $closes" val isOpenNow = - remember(now) { hour.isOpenAt(now.toLocalTime()) && isOpenToday } + remember(now) { hour.isOpenAt(now.time) && isOpenToday } when { isOpenNow -> Text( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt index 5aadb6ec..dd6c628a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt @@ -19,7 +19,6 @@ package de.gematik.ti.erp.app.pharmacy.ui import android.util.Patterns -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.layout.Arrangement @@ -34,8 +33,6 @@ import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -53,16 +50,21 @@ import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.max import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.ui.model.addressSupplementInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.deliveryInformationInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.mailInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.nameInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.phoneNumberInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.postalCodeAndCityInputField +import de.gematik.ti.erp.app.pharmacy.ui.model.streetAndNumberInputField import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.BottomAppBar import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.InputField import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerSmall @@ -72,7 +74,6 @@ import kotlinx.coroutines.launch const val StringLengthLimit = 100 const val MinPhoneLength = 4 -@Suppress("LongMethod") @Composable fun EditShippingContactScreen( orderState: PharmacyOrderState, @@ -99,33 +100,18 @@ fun EditShippingContactScreen( val codeAndCityError by remember(contact) { derivedStateOf { contact.postalCodeAndCity.isBlank() } } val mailError by remember(contact) { derivedStateOf { !isMailValid(contact.mail) } } - if (showBackAlert) { - CommonAlertDialog( - header = stringResource(R.string.edit_contact_back_alert_header), - info = stringResource(R.string.edit_contact_back_alert_information), - onCancel = { showBackAlert = false }, - onClickAction = onBack, - cancelText = stringResource(R.string.edit_contact_back_alert_change), - actionText = stringResource(R.string.edit_contact_back_alert_action) - ) - } + if (showBackAlert) { BackAlert(onCancel = { showBackAlert = false }, onBack = onBack) } AnimatedElevationScaffold( navigationMode = NavigationBarMode.Back, bottomBar = { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(Modifier.weight(1f)) - Button( - onClick = { - orderState.onSaveContact(contact) - onBack() - }, - enabled = !telephoneError && !mailError && !nameError && !line1Error && !codeAndCityError - ) { - Text(stringResource(R.string.edit_shipping_contact_save)) + ContactBottomBar( + enabled = !telephoneError && !mailError && !nameError && !line1Error && !codeAndCityError, + onClick = { + orderState.onSaveContact(contact) + onBack() } - SpacerSmall() - } + ) }, topBarTitle = stringResource(R.string.edit_shipping_contact_top_bar_title), listState = listState, @@ -156,129 +142,116 @@ fun EditShippingContactScreen( end = PaddingDefaults.Medium ) ) { - item { - Text( - stringResource(R.string.edit_shipping_contact_title_contact), - style = AppTheme.typography.h6 - ) - } - item(key = "InputField_1") { - InputField( - modifier = Modifier - .scrollOnFocus(1, listState) - .fillParentMaxWidth(), - value = contact.telephoneNumber, - onValueChange = { phone -> - contact = contact.copy( - telephoneNumber = phone.trim().take(StringLengthLimit) - ) - }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { - Text( - if (telephoneOptional) { - stringResource(R.string.edit_shipping_contact_phone_optional) - } else { - stringResource(R.string.edit_shipping_contact_phone) - } - ) - }, - isError = telephoneError, - errorText = { Text(stringResource(R.string.edit_shipping_contact_error_phone)) }, - keyBoardType = KeyboardType.Phone - ) - } - item(key = "InputField_2") { - InputField( - modifier = Modifier - .scrollOnFocus(2, listState) - .fillParentMaxWidth(), - value = contact.mail, - onValueChange = { mail -> contact = (contact.copy(mail = mail.take(StringLengthLimit))) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { Text(stringResource(R.string.edit_shipping_contact_mail)) }, - isError = mailError, - keyBoardType = KeyboardType.Email - ) - } - item { - SpacerLarge() - Text( - stringResource(R.string.edit_shipping_contact_title_address), - style = AppTheme.typography.h6 - ) - } - item(key = "InputField_3") { - InputField( - modifier = Modifier - .scrollOnFocus(4, listState) - .fillParentMaxWidth(), - value = contact.name, - onValueChange = { name -> contact = (contact.copy(name = name.take(StringLengthLimit))) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { Text(stringResource(R.string.edit_shipping_contact_name)) }, - isError = nameError, - errorText = { Text(stringResource(R.string.edit_shipping_contact_error_name)) } - ) - } - item(key = "InputField_4") { - InputField( - modifier = Modifier - .scrollOnFocus(5, listState) - .fillParentMaxWidth(), - value = contact.line1, - onValueChange = { line1 -> contact = (contact.copy(line1 = line1.take(StringLengthLimit))) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { Text(stringResource(R.string.edit_shipping_contact_title_line1)) }, - isError = line1Error, - errorText = { Text(stringResource(R.string.edit_shipping_contact_error_line1)) } - ) - } - item(key = "InputField_5") { - InputField( - modifier = Modifier - .scrollOnFocus(6, listState) - .fillParentMaxWidth(), - value = contact.line2, - onValueChange = { line2 -> contact = (contact.copy(line2 = line2.take(StringLengthLimit))) }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { Text(stringResource(R.string.edit_shipping_contact_line2)) }, - isError = false - ) - } - item(key = "InputField_6") { - InputField( - modifier = Modifier - .scrollOnFocus(7, listState) - .fillParentMaxWidth(), - value = contact.postalCodeAndCity, - onValueChange = { postalCodeAndCity -> - contact = (contact.copy(postalCodeAndCity = postalCodeAndCity.take(StringLengthLimit))) - }, - onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, - label = { Text(stringResource(R.string.edit_shipping_contact_postal_code_and_city)) }, - isError = codeAndCityError, - errorText = { Text(stringResource(R.string.edit_shipping_contact_error_postal_code_and_city)) } - ) - } - item(key = "InputField_7") { - InputField( - modifier = Modifier - .scrollOnFocus(8, listState) - .fillParentMaxWidth(), - value = contact.deliveryInformation, - onValueChange = { deliveryInformation -> - contact = (contact.copy(deliveryInformation = deliveryInformation.take(StringLengthLimit))) - }, - onSubmit = { focusManager.clearFocus() }, - label = { Text(stringResource(R.string.edit_shipping_contact_delivery_information)) }, - isError = false - ) - } + item { ContactHeader() } + phoneNumberInputField( + listState = listState, + value = contact.telephoneNumber, + telephoneOptional = telephoneOptional, + isError = telephoneError, + onValueChange = { phone -> + contact = contact.copy( + telephoneNumber = phone.trim().take(StringLengthLimit) + ) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + mailInputField( + listState = listState, + value = contact.mail, + onValueChange = { mail -> contact = (contact.copy(mail = mail.take(StringLengthLimit))) }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + isError = mailError + ) + item { AddressHeader() } + + nameInputField( + listState = listState, + value = contact.name, + onValueChange = { name -> contact = (contact.copy(name = name.take(StringLengthLimit))) }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + isError = nameError + ) + + streetAndNumberInputField( + listState = listState, + value = contact.line1, + onValueChange = { line1 -> contact = (contact.copy(line1 = line1.take(StringLengthLimit))) }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + isError = line1Error + ) + + addressSupplementInputField( + listState = listState, + value = contact.line2, + onValueChange = { line2 -> contact = (contact.copy(line2 = line2.take(StringLengthLimit))) }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) } + ) + + postalCodeAndCityInputField( + listState = listState, + value = contact.postalCodeAndCity, + onValueChange = { postalCodeAndCity -> + contact = (contact.copy(postalCodeAndCity = postalCodeAndCity.take(StringLengthLimit))) + }, + onSubmit = { focusManager.moveFocus(FocusDirection.Down) }, + isError = codeAndCityError + ) + + deliveryInformationInputField( + listState = listState, + value = contact.deliveryInformation, + onValueChange = { deliveryInformation -> + contact = (contact.copy(deliveryInformation = deliveryInformation.take(StringLengthLimit))) + }, + onSubmit = { focusManager.clearFocus() } + ) + } + } +} + +@Composable +fun ContactBottomBar(enabled: Boolean, onClick: () -> Unit) { + BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { + Spacer(Modifier.weight(1f)) + Button( + onClick = onClick, + enabled = enabled + ) { + Text(stringResource(R.string.edit_shipping_contact_save)) } + SpacerSmall() } } +@Composable +fun AddressHeader() { + SpacerLarge() + Text( + stringResource(R.string.edit_shipping_contact_title_address), + style = AppTheme.typography.h6 + ) +} + +@Composable +fun BackAlert(onCancel: () -> Unit, onBack: () -> Unit) { + CommonAlertDialog( + header = stringResource(R.string.edit_contact_back_alert_header), + info = stringResource(R.string.edit_contact_back_alert_information), + onCancel = onCancel, + onClickAction = onBack, + cancelText = stringResource(R.string.edit_contact_back_alert_change), + actionText = stringResource(R.string.edit_contact_back_alert_action) + ) +} + +@Composable +fun ContactHeader() { + Text( + stringResource(R.string.edit_shipping_contact_title_contact), + style = AppTheme.typography.h6 + ) +} + fun isMailValid(mail: String): Boolean { return mail.isEmpty() || (Patterns.EMAIL_ADDRESS.matcher(mail).matches() && mail.length <= StringLengthLimit) } @@ -323,37 +296,3 @@ fun Modifier.scrollOnFocus(to: Int, listState: LazyListState, offset: Int = 0) = } } } - -@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) -fun Modifier.scrollOnFocus() = composed { - val coroutineScope = rememberCoroutineScope() - val mutex = MutatorMutex() - val bringIntoViewRequester = remember { BringIntoViewRequester() } - - var hasFocus by remember { mutableStateOf(false) } - val keyboardVisible = WindowInsets.isImeVisible - - LaunchedEffect(hasFocus, keyboardVisible) { - if (hasFocus && keyboardVisible) { - mutex.mutate { - delay(LayoutDelay) - bringIntoViewRequester.bringIntoView() - } - } - } - - bringIntoViewRequester(bringIntoViewRequester) - .onFocusChanged { - if (it.hasFocus) { - hasFocus = true - coroutineScope.launch { - mutex.mutate(MutatePriority.UserInput) { - delay(LayoutDelay) - bringIntoViewRequester.bringIntoView() - } - } - } else { - hasFocus = false - } - } -} 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 8d3ccb06..53470888 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 @@ -77,6 +77,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -101,6 +102,7 @@ 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.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.usecase.model.PharmacyUseCaseData @@ -190,7 +192,11 @@ fun MapsOverview( position = CameraPosition.fromLatLngZoom(latLng, DefaultZoomLevel) } - var pharmacies by remember { mutableStateOf>(emptyList()) } + var pharmacies by rememberSaveable(saver = complexAutoSaver()) { + mutableStateOf>( + emptyList() + ) + } LaunchedEffect(Unit) { searchController .pharmacyMapsFlow @@ -207,7 +213,7 @@ fun MapsOverview( } } - var showSearchButton by remember { mutableStateOf(false) } + var showSearchButton by rememberSaveable { mutableStateOf(false) } CameraAnimation( cameraPositionState = cameraPositionState, pharmacySearchController = searchController, @@ -288,13 +294,15 @@ fun MapsOverview( @OptIn(ExperimentalMaterialApi::class) @Stable class PharmacySheetState( - private val scope: CoroutineScope + content: PharmacySearchSheetContentState ) : SwipeableState( initialValue = ModalBottomSheetValue.Hidden, animationSpec = SwipeableDefaults.AnimationSpec, confirmStateChange = { true } ) { - var content: PharmacySearchSheetContentState by mutableStateOf(PharmacySearchSheetContentState.FilterSelected) + lateinit var scope: CoroutineScope + + var content: PharmacySearchSheetContentState by mutableStateOf(content) private set fun show(content: PharmacySearchSheetContentState, snap: Boolean = false) { @@ -324,8 +332,9 @@ fun rememberPharmacySheetState( content: PharmacySearchSheetContentState? = null ): PharmacySheetState { val scope = rememberCoroutineScope() - val state = remember { - PharmacySheetState(scope) + val state = rememberSaveable(saver = complexAutoSaver(init = { this.scope = scope })) { + PharmacySheetState(content ?: PharmacySearchSheetContentState.FilterSelected) + .apply { this.scope = scope } } LaunchedEffect(content) { content?.let { state.show(content, snap = true) } 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 2952a9d9..952b97ac 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 @@ -18,18 +18,16 @@ package de.gematik.ti.erp.app.pharmacy.ui -import android.os.Parcelable import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.App +import de.gematik.ti.erp.app.DispatchProvider +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 @@ -43,7 +41,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import org.kodein.di.compose.rememberInstance @Stable @@ -132,62 +129,19 @@ class PharmacyOrderState( } } -@Parcelize -private data class PharmacyOrderStateSavedState( - val selectedPharmacyTelematikID: String?, - val selectedOrderOption: PharmacyScreenData.OrderOption? -) : Parcelable - -private fun pharmacyOrderStateSaver( - profileId: ProfileIdentifier, - useCase: PharmacySearchUseCase, - scope: CoroutineScope -): Saver = Saver( - save = { orderState -> - orderState.selectedPharmacy?.telematikId?.let { - App.cache.store("pharmacyOrderState-$it", orderState.selectedPharmacy) - } - PharmacyOrderStateSavedState( - selectedPharmacyTelematikID = orderState.selectedPharmacy?.telematikId, - selectedOrderOption = orderState.selectedOrderOption - ) - }, - restore = { savedState -> - PharmacyOrderState( - profileId, - useCase, - scope - ).apply { - val pharmacy = savedState.selectedPharmacyTelematikID?.let { - App.cache.recover("pharmacyOrderState-$it") as PharmacyUseCaseData.Pharmacy? - } - - pharmacy?.let { - savedState.selectedOrderOption?.let { - onSelectPharmacy(pharmacy, savedState.selectedOrderOption) - } - } - } - } -) - @Composable fun rememberPharmacyOrderState(): PharmacyOrderState { val activeProfile = LocalProfileHandler.current.activeProfile val useCase by rememberInstance() - val scope = rememberCoroutineScope() + val dispatchProvider by rememberInstance() return rememberSaveable( activeProfile.id, - saver = pharmacyOrderStateSaver( - activeProfile.id, - useCase, - scope - ) + saver = complexAutoSaver() ) { PharmacyOrderState( activeProfile.id, useCase, - scope + CoroutineScope(dispatchProvider.Default) ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt index a0a8ecf1..b378c379 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt @@ -67,8 +67,10 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import org.kodein.di.compose.rememberInstance -import java.time.OffsetDateTime private const val WaitForLocationUpdate = 2500L private const val DefaultRadiusInMeter = 999 * 1000.0 @@ -141,7 +143,9 @@ class PharmacySearchController( if (searchData.filter.openNow) { when { it.openingHours == null -> false - it.openingHours.isOpenAt(OffsetDateTime.now()) -> true + it.openingHours.isOpenAt( + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + ) -> true else -> false } } else { @@ -196,7 +200,9 @@ class PharmacySearchController( if (searchData.filter.openNow) { when { it.openingHours == null -> false - it.openingHours.isOpenAt(OffsetDateTime.now()) -> true + it.openingHours.isOpenAt( + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) + ) -> true else -> false } } else { 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 d73d092f..22b115ca 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 @@ -116,10 +116,13 @@ import de.gematik.ti.erp.app.utils.compose.Chip import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import java.text.DecimalFormat -import java.time.OffsetDateTime private const val OneKilometerInMeter = 1000 @@ -514,7 +517,7 @@ private fun PharmacyResultCard( ) val pharmacyLocalServices = pharmacy.provides.find { it is LocalPharmacyService } as LocalPharmacyService - val now = OffsetDateTime.now() + val now = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) } if (pharmacyLocalServices.isOpenAt(now)) { val text = if (pharmacyLocalServices.isAllDayOpen(now.dayOfWeek)) { @@ -637,15 +640,7 @@ fun PharmacySearchResultScreen( val scope = rememberCoroutineScope() - val locationPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - scope.launch { - searchController.search( - name = searchName, - filter = searchFilter.copy(nearBy = permissions.values.any { it }) - ) - } - } + val locationPermissionLauncher = getLocationPermissionLauncher(scope, searchController, searchName, searchFilter) var showNoLocationDialog by remember { mutableStateOf(false) } if (showNoLocationDialog) { @@ -678,15 +673,17 @@ fun PharmacySearchResultScreen( } val loadState = searchPagingItems.loadState - val isLoading by derivedStateOf { - searchController.isLoading || listOf(loadState.prepend, loadState.append, loadState.refresh) - .any { - when (it) { - is LoadState.NotLoading -> false // initial ui only loading indicator - is LoadState.Loading -> true - else -> false + val isLoading by remember { + derivedStateOf { + searchController.isLoading || listOf(loadState.prepend, loadState.append, loadState.refresh) + .any { + when (it) { + is LoadState.NotLoading -> false // initial ui only loading indicator + is LoadState.Loading -> true + else -> false + } } - } + } } val focusManager = LocalFocusManager.current @@ -820,6 +817,21 @@ fun PharmacySearchResultScreen( } } +@Composable +private fun getLocationPermissionLauncher( + scope: CoroutineScope, + searchController: PharmacySearchController, + searchName: String, + searchFilter: PharmacyUseCaseData.Filter +) = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + scope.launch { + searchController.search( + name = searchName, + filter = searchFilter.copy(nearBy = permissions.values.any { it }) + ) + } +} + @Composable private fun SearchResultContent( searchPagingItems: LazyPagingItems, @@ -834,19 +846,23 @@ private fun SearchResultContent( .padding(PaddingDefaults.Medium) val loadState = searchPagingItems.loadState - val showNothingFound by derivedStateOf { - listOf(loadState.prepend, loadState.append) - .all { - when (it) { - is LoadState.NotLoading -> - it.endOfPaginationReached && searchPagingItems.itemCount == 0 + val showNothingFound by remember { + derivedStateOf { + listOf(loadState.prepend, loadState.append) + .all { + when (it) { + is LoadState.NotLoading -> + it.endOfPaginationReached && searchPagingItems.itemCount == 0 - else -> false - } - } && loadState.refresh is LoadState.NotLoading + else -> false + } + } && loadState.refresh is LoadState.NotLoading + } } - val showError by derivedStateOf { searchPagingItems.itemCount <= 1 && loadState.refresh is LoadState.Error } + val showError by remember { + derivedStateOf { searchPagingItems.itemCount <= 1 && loadState.refresh is LoadState.Error } + } LazyColumn( modifier = Modifier @@ -895,21 +911,13 @@ private fun SearchResultContent( } itemsIndexed(searchPagingItems) { index, pharmacy -> if (pharmacy != null) { - Column { - PharmacyResultCard( - modifier = itemPaddingModifier - .semantics { - pharmacyId = pharmacy.telematikId - } - .testTag(TestTag.PharmacySearch.PharmacyListEntry), - pharmacy = pharmacy - ) { - onSelectPharmacy(pharmacy) - } - if (index < searchPagingItems.itemCount - 1) { - Divider(startIndent = PaddingDefaults.Medium) - } - } + PharmacySearchResult( + itemPaddingModifier, + index, + searchPagingItems.itemCount, + pharmacy, + onSelectPharmacy + ) } } if (loadState.append is LoadState.Error) { @@ -927,3 +935,28 @@ private fun SearchResultContent( } } } + +@Composable +fun PharmacySearchResult( + modifier: Modifier, + index: Int, + itemCount: Int, + pharmacy: PharmacyUseCaseData.Pharmacy, + onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit +) { + Column { + PharmacyResultCard( + modifier = modifier + .semantics { + pharmacyId = pharmacy.telematikId + } + .testTag(TestTag.PharmacySearch.PharmacyListEntry), + pharmacy = pharmacy + ) { + onSelectPharmacy(pharmacy) + } + if (index < itemCount - 1) { + Divider(startIndent = PaddingDefaults.Medium) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt new file mode 100644 index 00000000..c9c488e6 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt @@ -0,0 +1,196 @@ +/* + * 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.pharmacy.ui.model + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus +import de.gematik.ti.erp.app.utils.compose.InputField + +fun LazyListScope.phoneNumberInputField( + listState: LazyListState, + value: String, + telephoneOptional: Boolean, + isError: Boolean, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit +) { + item(key = "InputField_1") { + InputField( + modifier = Modifier + .scrollOnFocus(1, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { + Text( + if (telephoneOptional) { + stringResource(R.string.edit_shipping_contact_phone_optional) + } else { + stringResource(R.string.edit_shipping_contact_phone) + } + ) + }, + isError = isError, + errorText = { Text(stringResource(R.string.edit_shipping_contact_error_phone)) }, + keyBoardType = KeyboardType.Phone + ) + } +} + +fun LazyListScope.mailInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit, + isError: Boolean +) { + item(key = "InputField_2") { + InputField( + modifier = Modifier + .scrollOnFocus(2, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_mail)) }, + isError = isError, + keyBoardType = KeyboardType.Email + ) + } +} + +@Suppress("MagicNumber") +fun LazyListScope.nameInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit, + isError: Boolean +) { + item(key = "InputField_3") { + InputField( + modifier = Modifier + .scrollOnFocus(4, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_name)) }, + isError = isError, + errorText = { Text(stringResource(R.string.edit_shipping_contact_error_name)) } + ) + } +} + +@Suppress("MagicNumber") +fun LazyListScope.streetAndNumberInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit, + isError: Boolean +) { + item(key = "InputField_4") { + InputField( + modifier = Modifier + .scrollOnFocus(5, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_title_line1)) }, + isError = isError, + errorText = { Text(stringResource(R.string.edit_shipping_contact_error_line1)) } + ) + } +} + +@Suppress("MagicNumber") +fun LazyListScope.addressSupplementInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit +) { + item(key = "InputField_5") { + InputField( + modifier = Modifier + .scrollOnFocus(6, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_line2)) }, + isError = false + ) + } +} + +@Suppress("MagicNumber") +fun LazyListScope.postalCodeAndCityInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit, + isError: Boolean +) { + item(key = "InputField_6") { + InputField( + modifier = Modifier + .scrollOnFocus(7, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_postal_code_and_city)) }, + isError = isError, + errorText = { Text(stringResource(R.string.edit_shipping_contact_error_postal_code_and_city)) } + ) + } +} + +@Suppress("MagicNumber") +fun LazyListScope.deliveryInformationInputField( + listState: LazyListState, + value: String, + onValueChange: (String) -> Unit, + onSubmit: (String) -> Unit +) { + item(key = "InputField_7") { + InputField( + modifier = Modifier + .scrollOnFocus(8, listState) + .fillParentMaxWidth(), + value = value, + onValueChange = onValueChange, + onSubmit = onSubmit, + label = { Text(stringResource(R.string.edit_shipping_contact_delivery_information)) }, + isError = false + ) + } +} 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 528c6d4d..a72a9b32 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 @@ -195,7 +195,7 @@ class PharmacySearchUseCase( PharmacyUseCaseData.PrescriptionOrder( taskId = task.taskId, accessCode = task.accessCode!!, - title = task.medicationRequestMedicationName(), + title = task.medicationName(), timestamp = task.authoredOn, substitutionsAllowed = false ) 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 7d26ee58..a75c0481 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 @@ -26,7 +26,7 @@ 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.PharmacyService import kotlinx.parcelize.Parcelize -import java.time.Instant +import kotlinx.datetime.Instant private const val DefaultRadiusInMeter = 999 * 1000.0 diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt index f96d33e3..16549ca7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt @@ -36,10 +36,12 @@ 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.model.SyncedTaskData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import java.time.LocalDateTime -import java.time.ZoneOffset +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -82,12 +84,12 @@ fun AccidentInformation( val text = if (isAccident) { remember(LocalConfiguration.current, prescription.medicationRequest.dateOfAccident) { val dtFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - prescription.medicationRequest.dateOfAccident?.let { - LocalDateTime - .ofInstant(it, ZoneOffset.UTC) - .toLocalDate() - .format(dtFormatter) - } ?: MissingValue + prescription.medicationRequest.dateOfAccident + ?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.date + ?.toJavaLocalDate() + ?.format(dtFormatter) + ?: MissingValue } } else { NoInfo 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 2b7b109b..a5263caf 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 @@ -45,8 +45,8 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.dateTimeMediumText -import java.time.Instant -import java.time.temporal.ChronoUnit +import kotlinx.datetime.Instant +import kotlin.time.Duration.Companion.days sealed class PrescriptionDetailBottomSheetContent { @Stable @@ -119,10 +119,7 @@ fun PrescriptionDetailInfoSheetContent( SpacerMedium() DateRange( start = remember { - infoContent.prescription.acceptUntil?.plus( - 1, - ChronoUnit.DAYS - ) ?: start + infoContent.prescription.acceptUntil?.plus(1.days) ?: start }, end = infoContent.prescription.expiresOn ?: start ) 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 ffec4129..c5e0ccf0 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 @@ -39,6 +39,7 @@ import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt index 0e154c51..5e4fff34 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt @@ -35,6 +35,7 @@ import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt index 2a8c0a3c..f2666095 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt @@ -40,12 +40,11 @@ import de.gematik.ti.erp.app.insuranceState import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.repository.statusMapping import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle +import de.gematik.ti.erp.app.utils.temporalText +import kotlinx.datetime.TimeZone @Composable fun PatientScreen( @@ -97,12 +96,8 @@ fun PatientScreen( Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.BirthDate), text = remember(LocalConfiguration.current, patient) { - val dtFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - patient.birthdate?.let { - LocalDateTime - .ofInstant(it, ZoneOffset.UTC) - .format(dtFormatter) + temporalText(it, TimeZone.currentSystemDefault()) } ?: noValueText }, label = stringResource(R.string.pres_detail_patient_label_birthdate) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt index 90d55b91..36829b61 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium 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 c236b836..438aec7b 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,12 +20,9 @@ package de.gematik.ti.erp.app.prescription.detail.ui -import android.widget.Toast -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -46,7 +43,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.material.ButtonDefaults import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -73,13 +69,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset @@ -104,15 +97,15 @@ import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog +import de.gematik.ti.erp.app.utils.compose.HealthPortalLink +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall import de.gematik.ti.erp.app.utils.compose.PrimaryButtonTiny 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.SpacerXLarge import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.provideEmailIntent @@ -371,8 +364,10 @@ private fun DeleteAction( var dropdownExpanded by remember { mutableStateOf(false) } - val isDeletable by derivedStateOf { - (prescription as? PrescriptionData.Synced)?.isDeletable ?: true + val isDeletable by remember { + derivedStateOf { + (prescription as? PrescriptionData.Synced)?.isDeletable ?: true + } } IconButton( @@ -436,7 +431,6 @@ private fun DeleteAction( } } -@Suppress("LongMethod") @Composable private fun SyncedPrescriptionOverview( navController: NavController, @@ -461,9 +455,6 @@ private fun SyncedPrescriptionOverview( .testTag(TestTag.Prescriptions.Details.Content), contentPadding = colPadding ) { - // prescription name - // prescription kind - // prescription state item { SyncedHeader( prescription = prescription, @@ -472,51 +463,22 @@ private fun SyncedPrescriptionOverview( } item { - val text = when { - prescription.medicationRequest.additionalFee == SyncedTaskData.AdditionalFee.Exempt -> - stringResource(R.string.pres_detail_no) - - prescription.medicationRequest.additionalFee == SyncedTaskData.AdditionalFee.NotExempt -> - stringResource(R.string.pres_detail_yes) + val text = additionalFeeText(prescription.medicationRequest.additionalFee) ?: noValueText - else -> noValueText - } Label( text = text, label = stringResource(R.string.pres_details_additional_fee), - onClick = { - when { - prescription.medicationRequest.additionalFee == SyncedTaskData.AdditionalFee.NotExempt -> - onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt) - - prescription.medicationRequest.additionalFee == SyncedTaskData.AdditionalFee.Exempt -> - onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeExempt) - - else -> {} - } - } + onClick = onClickAdditionalFee(prescription.medicationRequest.additionalFee, onShowInfo) ) } - prescription.medicationRequest.emergencyFee?.let { + prescription.medicationRequest.emergencyFee?.let { emergencyFee -> item { - // false - emergencyFee fee is to be paid by the insured (default value) - // true - emergencyFee fee is not to be paid by the insured but by the payer - val text = if (it) { - stringResource(R.string.pres_detail_no) - } else { - stringResource(R.string.pres_detail_yes) - } + val text = emergencyFeeText(emergencyFee) Label( text = text, label = stringResource(R.string.pres_details_emergency_fee), - onClick = { - if (it) { - onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt) - } else { - onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFee) - } - } + onClick = onClickEmergencyFee(emergencyFee, onShowInfo) ) } } @@ -526,13 +488,7 @@ private fun SyncedPrescriptionOverview( modifier = Modifier.testTag(TestTag.Prescriptions.Details.MedicationButton), text = prescription.name ?: noValueText, label = stringResource(R.string.pres_details_medication), - onClick = { - if (!prescription.isDispensed) { - onSelectMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) - } else { - navController.navigate(PrescriptionDetailsNavigationScreens.MedicationOverview.path()) - } - } + onClick = onClickMedication(prescription, onSelectMedication, navController) ) } @@ -609,6 +565,63 @@ private fun SyncedPrescriptionOverview( } } +@Composable +private fun onClickMedication( + prescription: PrescriptionData.Synced, + onSelectMedication: (PrescriptionData.Medication) -> Unit, + navController: NavController +): () -> Unit = { + if (!prescription.isDispensed) { + onSelectMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) + } else { + navController.navigate(PrescriptionDetailsNavigationScreens.MedicationOverview.path()) + } +} + +@Composable +private fun onClickEmergencyFee( + emergencyFee: Boolean, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +): () -> Unit = { + if (emergencyFee) { + onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt) + } else { + onShowInfo(PrescriptionDetailBottomSheetContent.EmergencyFee) + } +} + +@Composable +private fun emergencyFeeText(emergencyFee: Boolean) = if (emergencyFee) { + // false - emergencyFee fee is to be paid by the insured (default value) + // true - emergencyFee fee is not to be paid by the insured but by the payer + stringResource(R.string.pres_detail_no) +} else { + stringResource(R.string.pres_detail_yes) +} + +@Composable +private fun onClickAdditionalFee( + additionalFee: SyncedTaskData.AdditionalFee, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +): () -> Unit = { + when (additionalFee) { + SyncedTaskData.AdditionalFee.NotExempt -> + onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt) + SyncedTaskData.AdditionalFee.Exempt -> + onShowInfo(PrescriptionDetailBottomSheetContent.AdditionalFeeExempt) + else -> {} + } +} + +@Composable +private fun additionalFeeText(additionalFee: SyncedTaskData.AdditionalFee): String? = when (additionalFee) { + SyncedTaskData.AdditionalFee.Exempt -> + stringResource(R.string.pres_detail_no) + SyncedTaskData.AdditionalFee.NotExempt -> + stringResource(R.string.pres_detail_yes) + else -> null +} + @Composable private fun FailureBanner( modifier: Modifier, @@ -778,9 +791,6 @@ private fun ScannedPrescriptionOverview( .fillMaxSize(), contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() ) { - // prescription name - // prescription kind - // prescription state item { Column( Modifier @@ -853,91 +863,3 @@ private fun RedeemedButton( Text(buttonText) } } - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun Label( - modifier: Modifier = Modifier, - text: String?, - label: String? = null, - onClick: (() -> Unit)? = null -) { - val clipboardManager = LocalClipboardManager.current - val context = LocalContext.current - - val verticalPadding = if (label != null) { - PaddingDefaults.ShortMedium - } else { - PaddingDefaults.Medium - } - - val noValueText = stringResource(R.string.pres_details_no_value) - - Row( - modifier = modifier - .combinedClickable( - onClick = { - onClick?.invoke() - }, - onLongClick = { - if (text != null) { - clipboardManager.setText(AnnotatedString(text)) - Toast - .makeText(context, "$label $text", Toast.LENGTH_SHORT) - .show() - } - }, - role = Role.Button - ) - .padding(horizontal = PaddingDefaults.Medium, vertical = verticalPadding) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Column(Modifier.weight(1f)) { - Text( - text = text ?: noValueText, - style = AppTheme.typography.body1 - ) - if (label != null) { - Text( - text = label, - style = AppTheme.typography.body2l - ) - } - } - if (onClick != null) { - SpacerMedium() - Icon(Icons.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) - } - } -} - -@Composable -fun HealthPortalLink( - modifier: Modifier -) { - Column(modifier = modifier) { - Text( - text = stringResource(R.string.pres_detail_health_portal_description), - style = AppTheme.typography.body2l - ) - - val linkInfo = stringResource(R.string.pres_detail_health_portal_description_url_info) - val link = stringResource(R.string.pres_detail_health_portal_description_url) - val uriHandler = LocalUriHandler.current - val annotatedLink = annotatedLinkStringLight(link, linkInfo) - - SpacerSmall() - ClickableText( - text = annotatedLink, - onClick = { - annotatedLink - .getStringAnnotations("URL", it, it) - .firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) - } - }, - modifier = Modifier.align(Alignment.End) - ) - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt index 2bd793cb..e63c6517 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt @@ -39,12 +39,12 @@ import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.datetime.Clock import org.kodein.di.LazyDelegate import org.kodein.di.compose.rememberInstance import java.net.URI import java.net.URISyntaxException import java.net.URLEncoder -import java.time.Instant private const val ShareBaseUri = "https://das-e-rezept-fuer-deutschland.de/prescription/#" @@ -100,7 +100,7 @@ class SharePrescriptionController( profileId = profileId, taskId = taskId, accessCode = accessCode, - scannedOn = Instant.now(), + scannedOn = Clock.System.now(), redeemedOn = null ) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt index 7d9a7842..cf73606b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.semantics.semantics 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.fhir.parser.FhirTemporal import de.gematik.ti.erp.app.medicationCategory import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData @@ -49,14 +50,14 @@ import de.gematik.ti.erp.app.prescription.repository.normSizeMapping import de.gematik.ti.erp.app.substitutionAllowed import de.gematik.ti.erp.app.supplyForm import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.dateTimeMediumText import de.gematik.ti.erp.app.utils.temporalText -import java.time.Instant -import java.time.ZoneId -import java.time.temporal.TemporalAccessor +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone @Composable fun SyncedMedicationDetailScreen( @@ -160,7 +161,7 @@ fun LazyListScope.pznMedicationInformation(medication: SyncedTaskData.Medication item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Medication.Name), - text = medication.text, + text = medication.name(), label = stringResource(R.string.medication_trade_name) ) } @@ -208,7 +209,7 @@ fun LazyListScope.ingredientMedicationInformation( medication: SyncedTaskData.MedicationIngredient, onClickIngredient: (SyncedTaskData.Ingredient) -> Unit ) { - medication.ingredients.forEachIndexed { index, ingredient -> + medication.ingredients.forEach { ingredient -> item { IngredientNameLabel(ingredient.text) { onClickIngredient(ingredient) @@ -255,7 +256,7 @@ fun LazyListScope.compoundingMedicationInformation( ) { item { Label( - text = medication.text.takeIf { it.isNotEmpty() }, + text = medication.name(), label = stringResource(R.string.medication_compounding_name) ) } @@ -306,7 +307,7 @@ fun LazyListScope.compoundingMedicationInformation( fun LazyListScope.freeTextMedicationInformation(medication: SyncedTaskData.MedicationFreeText) { item { - Label(text = medication.text, label = stringResource(R.string.medication_freetext_name)) + Label(text = medication.name(), label = stringResource(R.string.medication_freetext_name)) } item { CategoryLabel(medication.category) @@ -413,6 +414,7 @@ fun CategoryLabel(category: SyncedTaskData.MedicationCategory) { SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL -> "00" SyncedTaskData.MedicationCategory.BTM -> "01" SyncedTaskData.MedicationCategory.AMVV -> "02" + SyncedTaskData.MedicationCategory.SONSTIGES -> "03" else -> null } } @@ -451,7 +453,7 @@ fun BvgLabel(bvg: Boolean) { @Composable fun AuthoredOnLabel(authoredOn: Instant) { Label( - text = remember { dateTimeMediumText(authoredOn, ZoneId.systemDefault()) }, + text = remember { dateTimeMediumText(authoredOn, TimeZone.currentSystemDefault()) }, label = stringResource(id = R.string.pres_detail_medication_label_authored_on) ) } @@ -515,9 +517,9 @@ fun HandedOverLabel(whenHandedOver: Instant) { } @Composable -fun ExpirationDateLabel(expirationDate: TemporalAccessor) { +fun ExpirationDateLabel(expirationDate: FhirTemporal) { Label( - text = remember { temporalText(expirationDate, ZoneId.systemDefault()) }, + text = remember { temporalText(expirationDate, TimeZone.currentSystemDefault()) }, label = stringResource(id = R.string.pres_detail_medication_label_expiration_date) ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt index b9be5bf7..18b9fdd6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt @@ -34,6 +34,7 @@ import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt index ab42dffe..a7ef9368 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt @@ -23,7 +23,7 @@ import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.time.Instant +import kotlinx.datetime.Instant object PrescriptionData { sealed interface Prescription { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt index 589f731f..b8d5f3d5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.serialization.json.JsonElement -import java.time.Instant +import kotlinx.datetime.Instant class LocalDataSource( private val realm: Realm @@ -110,7 +110,7 @@ class LocalDataSource( this.taskId = taskId this.communicationId = communicationId this.orderId = orderId ?: "" - this.sentOn = sentOn.toRealmInstant() + this.sentOn = sentOn.value.toRealmInstant() this.sender = sender this.recipient = recipient this.payload = payload diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt index 2ff6277a..2f4db896 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonElement -import java.time.Instant +import kotlinx.datetime.Instant enum class RemoteRedeemOption(val type: String) { Local(type = "onPremise"), diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt index c5feabfc..6c6deb2e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt @@ -29,9 +29,11 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData @@ -39,7 +41,9 @@ import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import java.time.ZoneId +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter @Composable @@ -58,7 +62,7 @@ fun ArchiveScreen(prescriptionViewModel: PrescriptionViewModel, navController: N } LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().testTag(TestTag.Prescriptions.Archive.Content), state = listState, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -66,14 +70,14 @@ fun ArchiveScreen(prescriptionViewModel: PrescriptionViewModel, navController: N state?.let { it.redeemedPrescriptions.forEachIndexed { index, prescription -> - item { + item(key = "prescription-${prescription.taskId}") { val previousPrescriptionRedeemedOn = it.redeemedPrescriptions.getOrNull(index - 1) ?.redeemedOrExpiredOn() - ?.atZone(ZoneId.systemDefault())?.toLocalDate() + ?.toLocalDateTime(TimeZone.currentSystemDefault()) val redeemedOn = prescription.redeemedOrExpiredOn() - .atZone(ZoneId.systemDefault()).toLocalDate() + .toLocalDateTime(TimeZone.currentSystemDefault()) val yearChanged = remember { previousPrescriptionRedeemedOn?.year != redeemedOn.year @@ -82,7 +86,7 @@ fun ArchiveScreen(prescriptionViewModel: PrescriptionViewModel, navController: N if (yearChanged) { val instantOfArchivedPrescription = remember { val dateFormatter = DateTimeFormatter.ofPattern("yyyy") - redeemedOn.format(dateFormatter) + redeemedOn.toJavaLocalDateTime().format(dateFormatter) } Text( 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 cc5c825e..5451a1be 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 @@ -66,7 +66,6 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.navigation.NavController @@ -99,12 +98,12 @@ import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.dateString import de.gematik.ti.erp.app.utils.compose.timeString -import java.time.Duration -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit const val ZERO_DAYS_LEFT = 0L const val ONE_DAY_LEFT = 1L @@ -121,7 +120,6 @@ fun PrescriptionScreen( ) { val profileHandler = LocalProfileHandler.current val profileId = profileHandler.activeProfile.id - var showUserNotAuthenticatedDialog by remember { mutableStateOf(false) } val onShowCardWall = { @@ -251,7 +249,10 @@ private fun PrescriptionsContent( if (state.redeemedPrescriptions.isNotEmpty()) { item { SpacerLarge() - TextButton(onClick = onClickArchive) { + TextButton( + onClick = onClickArchive, + modifier = Modifier.testTag(TestTag.Prescriptions.ArchiveButton) + ) { Text(stringResource(R.string.archived_prescriptions_button)) } SpacerLarge() @@ -351,127 +352,6 @@ private fun LazyListScope.prescriptionContent( } } -@Preview -@Composable -private fun FullDetailRecipeCardPreview() { - AppTheme { - Column { - FullDetailMedication( - modifier = Modifier, - prescription = - PrescriptionUseCaseData.Prescription.Synced( - "", - organization = "Medizinisches-Versorgungszentrum (MVZ) welches irgendeinen sehr langen Namen hat", - name = "Pantoprazol 40 mg - Medikament mit sehr vielen Namensbestandteilen", - authoredOn = Instant.now(), - isIncomplete = false, - redeemedOn = null, - expiresOn = Instant.now().plus(21, ChronoUnit.DAYS), - acceptUntil = Instant.now().minus(1, ChronoUnit.DAYS), - state = SyncedTaskData.SyncedTask.Other(SyncedTaskData.TaskStatus.InProgress, Instant.now()), - isDirectAssignment = false, - multiplePrescriptionState = PrescriptionUseCaseData.Prescription.MultiplePrescriptionState() - ), - onClick = {} - ) - SpacerMedium() - - FullDetailMedication( - modifier = Modifier, - prescription = - PrescriptionUseCaseData.Prescription.Synced( - organization = "Medizinisches-Versorgungszentrum (MVZ) welches irgendeinen sehr langen Namen hat", - taskId = "", - name = "Pantoprazol 40 mg", - isIncomplete = false, - authoredOn = Instant.now(), - redeemedOn = null, - expiresOn = Instant.now().plus(20, ChronoUnit.DAYS), - acceptUntil = Instant.now().plus(97, ChronoUnit.DAYS), - state = SyncedTaskData.SyncedTask.Other(SyncedTaskData.TaskStatus.Other, Instant.now()), - isDirectAssignment = false, - multiplePrescriptionState = PrescriptionUseCaseData.Prescription.MultiplePrescriptionState() - ), - onClick = {} - ) - SpacerMedium() - - FullDetailMedication( - modifier = Modifier, - prescription = - PrescriptionUseCaseData.Prescription.Synced( - organization = "Medizinisches-Versorgungszentrum (MVZ) welches irgendeinen sehr langen Namen hat", - taskId = "", - name = "Pantoprazol 40 mg", - isIncomplete = false, - authoredOn = Instant.now(), - redeemedOn = null, - expiresOn = Instant.now(), - acceptUntil = Instant.now().plus(1, ChronoUnit.DAYS), - state = SyncedTaskData.SyncedTask.Other(SyncedTaskData.TaskStatus.Completed, Instant.now()), - isDirectAssignment = false, - multiplePrescriptionState = PrescriptionUseCaseData.Prescription.MultiplePrescriptionState() - ), - onClick = {} - ) - SpacerMedium() - - FullDetailMedication( - modifier = Modifier, - prescription = - PrescriptionUseCaseData.Prescription.Synced( - organization = "Medizinisches-Versorgungszentrum (MVZ) welches irgendeinen sehr langen Namen hat", - taskId = "12344", - name = "Pantoprazol 40 mg", - authoredOn = Instant.now(), - isIncomplete = false, - redeemedOn = null, - expiresOn = Instant.now().minus(1, ChronoUnit.DAYS), - acceptUntil = Instant.now().minus(1, ChronoUnit.DAYS), - state = SyncedTaskData.SyncedTask.LaterRedeemable( - Instant.now().minus(1, ChronoUnit.DAYS) - ), - isDirectAssignment = false, - multiplePrescriptionState = PrescriptionUseCaseData.Prescription.MultiplePrescriptionState( - isPartOfMultiplePrescription = true, - numerator = "1", - denominator = "4", - start = Instant.now() - ) - ), - onClick = {} - ) - - SpacerMedium() - FullDetailMedication( - modifier = Modifier, - prescription = - PrescriptionUseCaseData.Prescription.Synced( - organization = "Medizinisches-Versorgungszentrum (MVZ) welches irgendeinen sehr langen Namen hat", - taskId = "12344", - name = "Medication", - authoredOn = Instant.now(), - isIncomplete = true, - redeemedOn = null, - expiresOn = Instant.now().minus(1, ChronoUnit.DAYS), - acceptUntil = Instant.now().minus(1, ChronoUnit.DAYS), - state = SyncedTaskData.SyncedTask.LaterRedeemable( - Instant.now().minus(1, ChronoUnit.DAYS) - ), - isDirectAssignment = false, - multiplePrescriptionState = PrescriptionUseCaseData.Prescription.MultiplePrescriptionState( - isPartOfMultiplePrescription = false, - numerator = null, - denominator = null, - start = null - ) - ), - onClick = {} - ) - } - } -} - @Composable fun readyPrescriptionStateInfo( acceptDaysLeft: Long, @@ -539,7 +419,7 @@ fun readyPrescriptionStateInfo( @Composable fun prescriptionStateInfo( state: SyncedTaskData.SyncedTask.TaskState, - now: Instant = Instant.now(), + now: Instant = Clock.System.now(), textAlign: TextAlign = TextAlign.Left ) { val warningAmber = mapOf( @@ -572,8 +452,8 @@ fun prescriptionStateInfo( } is SyncedTaskData.SyncedTask.Ready -> { - val expiryDaysLeft = remember { Duration.between(now, state.expiresOn).toDays() } - val acceptDaysLeft = remember { Duration.between(now, state.acceptUntil).toDays() } + val expiryDaysLeft = remember { (state.expiresOn - now).inWholeDays } + val acceptDaysLeft = remember { (state.acceptUntil - now).inWholeDays } val text = readyPrescriptionStateInfo(acceptDaysLeft, expiryDaysLeft) @@ -646,25 +526,25 @@ private fun sentOrCompletedPhrase(lastModified: Instant, now: Instant, completed is SentOrCompletedPhrase.RedeemedHoursAgo -> annotatedStringResource( R.string.received_on_minute, - remember { timeString(LocalDateTime.ofInstant(lastModified, ZoneId.systemDefault())) } + remember { timeString(lastModified.toLocalDateTime(TimeZone.currentSystemDefault())) } ).toString() is SentOrCompletedPhrase.SentHoursAgo -> annotatedStringResource( R.string.sent_on_minute, - remember { timeString(LocalDateTime.ofInstant(lastModified, ZoneId.systemDefault())) } + remember { timeString(lastModified.toLocalDateTime(TimeZone.currentSystemDefault())) } ).toString() is SentOrCompletedPhrase.RedeemedOn -> annotatedStringResource( R.string.received_on_day, - remember { dateString(LocalDateTime.ofInstant(phrase.on, ZoneId.systemDefault())) } + remember { dateString(phrase.on.toLocalDateTime(TimeZone.currentSystemDefault())) } ).toString() is SentOrCompletedPhrase.SentOn -> annotatedStringResource( R.string.sent_on_day, - remember { dateString(LocalDateTime.ofInstant(phrase.on, ZoneId.systemDefault())) } + remember { dateString(phrase.on.toLocalDateTime(TimeZone.currentSystemDefault())) } ).toString() } @@ -759,22 +639,6 @@ fun FullDetailMedication( } } -@Preview -@Composable -private fun LowDetailRecipeCardPreview() { - AppTheme { - LowDetailMedication( - Modifier, - prescription = PrescriptionUseCaseData.Prescription.Scanned( - "", - Instant.now(), - redeemedOn = Instant.now().plus(2, ChronoUnit.DAYS) - ), - onClick = {} - ) - } -} - @OptIn(ExperimentalMaterialApi::class) @Composable fun LowDetailMedication( @@ -785,13 +649,13 @@ fun LowDetailMedication( val dateFormatter = remember { DateTimeFormatter.ofPattern("dd.MM.yyyy") } val scannedOn = remember { - prescription.scannedOn.atZone(ZoneId.systemDefault()) - .toLocalDate().format(dateFormatter) + prescription.scannedOn.toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime().format(dateFormatter) } val redeemedOn = remember { - prescription.redeemedOn?.atZone(ZoneId.systemDefault()) - ?.toLocalDate()?.format(dateFormatter) + prescription.redeemedOn?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.toJavaLocalDateTime()?.format(dateFormatter) } val dateText = if (redeemedOn != null) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionsViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionsViewModel.kt index eebe6584..67e755a9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionsViewModel.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionsViewModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import java.time.Instant +import kotlinx.datetime.Clock private data class ScanWorkflow( val info: ScanScreenData.Info? = null, @@ -84,7 +84,7 @@ class ScanPrescriptionViewModel( private set private val emptyScanWorkflow = ScanWorkflow( - code = ScannedCode("", Instant.now()), + code = ScannedCode("", Clock.System.now()), coordinates = FloatArray(0), state = ScanScreenData.ScanState.Final ) @@ -113,7 +113,7 @@ class ScanPrescriptionViewModel( Pair( batch.averageScanTime, ScanWorkflow( - code = ScannedCode(json, Instant.now()), + code = ScannedCode(json, Clock.System.now()), coordinates = coords ) ) 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 e1ec4505..5fba575d 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 @@ -43,11 +43,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview 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.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerSmall @@ -117,7 +119,8 @@ fun ReadyStatusChip() = text = stringResource(R.string.prescription_status_ready), icon = null, textColor = AppTheme.colors.primary900, - backgroundColor = AppTheme.colors.primary100 + backgroundColor = AppTheme.colors.primary100, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionRedeemable) ) @Composable @@ -132,7 +135,8 @@ fun PendingStatusChip() = ) }, textColor = AppTheme.colors.neutral600, - backgroundColor = AppTheme.colors.neutral200 + backgroundColor = AppTheme.colors.neutral200, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionWaitForResponse) ) @Composable @@ -142,7 +146,8 @@ fun InProgressStatusChip() = icon = Icons.Rounded.HourglassTop, textColor = AppTheme.colors.yellow900, backgroundColor = AppTheme.colors.yellow100, - iconColor = AppTheme.colors.yellow500 + iconColor = AppTheme.colors.yellow500, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionInProgress) ) @Composable @@ -162,7 +167,8 @@ fun CompletedStatusChip() = icon = Icons.Rounded.DoneAll, textColor = AppTheme.colors.neutral600, backgroundColor = AppTheme.colors.neutral200, - iconColor = AppTheme.colors.neutral500 + iconColor = AppTheme.colors.neutral500, + modifier = Modifier.testTag(TestTag.Prescriptions.PrescriptionRedeemed) ) @Composable diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt index 9f61c815..c5b2901d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import io.github.aakira.napier.Napier -import java.time.Instant +import kotlinx.datetime.Instant data class ScannedCode( val json: String, diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt index 2be6b124..1e458e26 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt @@ -18,8 +18,7 @@ package de.gematik.ti.erp.app.prescription.ui.model -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Instant sealed interface SentOrCompletedPhrase { object RedeemedJustNow : SentOrCompletedPhrase @@ -39,8 +38,8 @@ private const val JustNowMinutes = 5L private const val MinutesAgo = 60L fun sentOrCompleted(lastModified: Instant, now: Instant, completed: Boolean = false): SentOrCompletedPhrase { - val dayDifference = Duration.between(lastModified, now).toDays() - val minDifference = Duration.between(lastModified, now).toMinutes() + val dayDifference = (now - lastModified).inWholeDays + val minDifference = (now - lastModified).inWholeMinutes return when { minDifference < JustNowMinutes -> if (completed) { SentOrCompletedPhrase.RedeemedJustNow diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt index 8844d5a2..194bee84 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt @@ -36,7 +36,8 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest import io.github.aakira.napier.Napier -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant class PrescriptionUseCase( private val repository: PrescriptionRepository, @@ -46,7 +47,7 @@ class PrescriptionUseCase( fun syncedActiveRecipes( profileId: ProfileIdentifier, - now: Instant = Instant.now() + now: Instant = Clock.System.now() ): Flow> = syncedTasks(profileId).map { tasks -> tasks.filter { it.isActive(now) } @@ -98,7 +99,7 @@ class PrescriptionUseCase( fun redeemedPrescriptions( profileId: ProfileIdentifier, - now: Instant = Instant.now() + now: Instant = Clock.System.now() ): Flow> = combine( scannedTasks(profileId), @@ -203,7 +204,7 @@ class PrescriptionUseCase( } suspend fun redeemScannedTask(taskId: String, redeem: Boolean) { - repository.updateRedeemedOn(taskId, if (redeem) Instant.now() else null) + repository.updateRedeemedOn(taskId, if (redeem) Clock.System.now() else null) } fun getAllTasksWithTaskIdOnly(): Flow> = diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt index 1285230f..ef6c0a33 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.prescription.usecase.model import androidx.compose.runtime.Immutable import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import java.time.Instant +import kotlinx.datetime.Instant object PrescriptionUseCaseData { /** diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt index 819f19f5..742a92ae 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt @@ -120,7 +120,9 @@ private fun AvatarPreview() { profile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), active = false, color = ProfilesData.ProfileColorNames.SUN_DEW, avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage, @@ -145,7 +147,9 @@ private fun AvatarWithSSOPreview() { profile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), active = false, color = ProfilesData.ProfileColorNames.SUN_DEW, diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt index 600c5568..643b6d17 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt @@ -94,7 +94,9 @@ import androidx.compose.foundation.layout.only import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentColor import androidx.compose.material.icons.rounded.AddAPhoto +import androidx.compose.material.icons.rounded.EuroSymbol import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.rememberScaffoldState import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector @@ -121,7 +123,7 @@ import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.visualTestTag import de.gematik.ti.erp.app.utils.sanitizeProfileName import kotlinx.coroutines.launch -import java.time.Instant +import kotlinx.datetime.Instant @Composable fun EditProfileScreen( @@ -197,6 +199,8 @@ fun EditProfileScreenContent( onClickPairedDevices: () -> Unit ) { val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + var showAddDefaultProfileDialog by remember { mutableStateOf(false) } var deleteProfileDialogVisible by remember { mutableStateOf(false) } @@ -220,6 +224,7 @@ fun EditProfileScreenContent( .visualTestTag(ProfileScreen), topBarTitle = stringResource(R.string.edit_profile_title), navigationMode = NavigationBarMode.Back, + scaffoldState = scaffoldState, listState = listState, actions = { ThreeDotMenu( @@ -261,6 +266,12 @@ fun EditProfileScreenContent( ) } + if (selectedProfile.insuranceInformation.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { + item { + ProfileInvoiceInformation {} + } + } + if (selectedProfile.ssoTokenScope != null) { item { ProfileEditPairedDeviceSection(onShowPairedDevices = onClickPairedDevices) @@ -280,6 +291,42 @@ fun EditProfileScreenContent( } } +@Composable +fun ProfileInvoiceInformation(onClick: () -> Unit) { + Column { + Text( + stringResource( + id = R.string.profile_invoiceInformation_header + ), + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + style = AppTheme.typography.h6 + ) + SpacerSmall() + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = onClick + ) + .padding(PaddingDefaults.Medium) + .semantics(mergeDescendants = true) {} + ) { + Icon(Icons.Rounded.EuroSymbol, null, tint = AppTheme.colors.primary600) + Text( + stringResource( + R.string.profile_show_invoices + ), + style = AppTheme.typography.body1 + ) + } + SpacerLarge() + Divider() + SpacerLarge() + } +} + @Composable fun ThreeDotMenu( selectedProfile: ProfilesUseCaseData.Profile, diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt index 72f37d23..7fae3155 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt @@ -61,7 +61,6 @@ import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.cardwall.mini.ui.NoneEnrolledException import de.gematik.ti.erp.app.cardwall.mini.ui.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException -import de.gematik.ti.erp.app.cardwall.ui.toAnnotatedString import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException @@ -77,6 +76,7 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.annotatedStringBold import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest @@ -87,9 +87,10 @@ import kotlinx.coroutines.launch import io.github.aakira.napier.Napier import java.io.IOException import java.net.UnknownHostException -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -455,21 +456,12 @@ private fun PairedDevice( } } -@Composable -fun localizedDateTimeString(timestamp: Instant, format: FormatStyle = FormatStyle.LONG): String { - val config = LocalConfiguration.current - return remember(config, format) { - val fmt = DateTimeFormatter.ofLocalizedDateTime(format) - LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()).format(fmt) - } -} - @Composable fun localizedDateString(timestamp: Instant, format: FormatStyle = FormatStyle.LONG): String { val config = LocalConfiguration.current return remember(config, format) { val fmt = DateTimeFormatter.ofLocalizedDate(format) - LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()).toLocalDate().format(fmt) + timestamp.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime().format(fmt) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt index b60cc841..7caac7cb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt @@ -44,6 +44,7 @@ import org.kodein.di.compose.rememberViewModel interface ProfileBridge { val profiles: Flow> suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) + suspend fun switchProfileToPKV(profile: ProfilesUseCaseData.Profile) } class ProfileViewModel( @@ -55,12 +56,18 @@ class ProfileViewModel( override suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) { profilesUseCase.switchActiveProfile(profile) } + + override suspend fun switchProfileToPKV(profile: ProfilesUseCaseData.Profile) { + profilesUseCase.switchProfileToPKV(profile) + } } val DefaultProfile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), active = false, color = ProfilesData.ProfileColorNames.SPRING_GRAY, lastAuthenticated = null, @@ -146,6 +153,10 @@ class ProfileHandler( suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) { bridge.switchActiveProfile(profile) } + + suspend fun switchProfileToPKV(profile: ProfilesUseCaseData.Profile) { + bridge.switchProfileToPKV(profile) + } } private fun profileHandlerSaver( diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt index c2284a59..c0791136 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.profiles.usecase +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.model.ProfilesData @@ -46,10 +47,15 @@ class ProfilesUseCase( ProfilesUseCaseData.Profile( id = profile.id, name = profile.name, - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation.ofNullable( - profile.insurantName, - profile.insuranceIdentifier, - profile.insuranceName + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insurantName = profile.insurantName ?: "", + insuranceIdentifier = profile.insuranceIdentifier ?: "", + insuranceName = profile.insuranceName ?: "", + insuranceType = when (profile.insuranceType) { + InsuranceTypeV1.None -> ProfilesUseCaseData.InsuranceType.NONE + InsuranceTypeV1.GKV -> ProfilesUseCaseData.InsuranceType.GKV + InsuranceTypeV1.PKV -> ProfilesUseCaseData.InsuranceType.PKV + } ), active = profile.active, color = profile.color, @@ -133,6 +139,9 @@ class ProfilesUseCase( } fun auditEvents(profileId: ProfileIdentifier) = auditRepository.auditEvents(profileId) + suspend fun switchProfileToPKV(profile: ProfilesUseCaseData.Profile) { + profilesRepository.switchProfileToPKV(profile.id) + } } fun sanitizedProfileName(profileName: String): String? = diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt index 5482f316..da46ba4e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt @@ -25,7 +25,7 @@ import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import java.time.Instant +import kotlinx.datetime.Instant class ProfilesWithPairedDevicesUseCase( private val idpUseCase: IdpUseCase, @@ -39,7 +39,7 @@ class ProfilesWithPairedDevicesUseCase( ProfilesUseCaseData.PairedDevice( name = raw.name, alias = pairingData.keyAliasOfSecureElement, - connectedOn = Instant.ofEpochSecond(raw.creationTime) + connectedOn = Instant.fromEpochSeconds(raw.creationTime) ) }.sortedByDescending { it.connectedOn diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt index 2d41451a..47a3a0a1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt @@ -23,24 +23,22 @@ import androidx.compose.runtime.Stable import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant object ProfilesUseCaseData { + + enum class InsuranceType { + GKV, + PKV, + NONE + } data class ProfileInsuranceInformation( val insurantName: String = "", val insuranceIdentifier: String = "", - val insuranceName: String = "" - ) { - companion object { - fun ofNullable( - insurantName: String?, - insuranceIdentifier: String?, - insuranceName: String? - ): ProfileInsuranceInformation { - return ProfileInsuranceInformation(insurantName ?: "", insuranceIdentifier ?: "", insuranceName ?: "") - } - } - } + val insuranceName: String = "", + val insuranceType: InsuranceType = InsuranceType.NONE + ) @Immutable data class Profile( @@ -54,7 +52,7 @@ object ProfilesUseCaseData { val lastAuthenticated: Instant? = null, val ssoTokenScope: IdpData.SingleSignOnTokenScope? ) { - fun ssoTokenValid(now: Instant = Instant.now()) = ssoTokenScope?.token?.isValid(now) ?: false + fun ssoTokenValid(now: Instant = Clock.System.now()) = ssoTokenScope?.token?.isValid(now) ?: false fun hasNoImageSelected() = this.avatarFigure == ProfilesData.AvatarFigure.PersonalizedImage && this.personalizedImage == null diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt index 663b6ead..79947c17 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import java.time.Instant +import kotlinx.datetime.Clock class RedeemUseCase( private val prescriptionRepository: PrescriptionRepository, @@ -50,7 +50,7 @@ class RedeemUseCase( }.flowOn(dispatchers.IO) suspend fun redeemScannedTasks(taskIds: List) { - val redeemedOn = Instant.now() + val redeemedOn = Clock.System.now() taskIds.forEach { taskId -> prescriptionRepository.updateRedeemedOn(taskId, redeemedOn) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt index f0403903..4d59c643 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt @@ -49,9 +49,11 @@ import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.phrasedDateString -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.LocalDateTime -import java.time.ZoneId @OptIn(ExperimentalFoundationApi::class) @Composable @@ -125,7 +127,8 @@ fun AuditEventsScreen( horizontalAlignment = Alignment.CenterHorizontally ) { val lastAuthenticatedDate = remember { - LocalDateTime.ofInstant(lastAuthenticated, ZoneId.systemDefault()) + lastAuthenticated?.toLocalDateTime(TimeZone.currentSystemDefault()) + ?.toJavaLocalDateTime() ?: LocalDateTime.MIN } Text( @@ -156,7 +159,9 @@ fun AuditEventsScreen( Text(auditEvent.description, style = AppTheme.typography.body2) val timestamp = remember { - LocalDateTime.ofInstant(auditEvent.timestamp, ZoneId.systemDefault()) + auditEvent.timestamp + .toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime() } Text( diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt index 89c2ef80..d157d5cc 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt @@ -80,7 +80,7 @@ class SettingsViewModel( MutableStateFlow(appPrefs.getBoolean(ScreenshotsAllowed, false)) fun screenState() = combine( - analytics.trackingAllowed, + analytics.analyticsAllowed, settingsUseCase.general, settingsUseCase.authenticationMode, screenshotsAllowed, 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 9b6f2857..a5761d0d 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 @@ -29,12 +29,14 @@ import de.gematik.ti.erp.app.settings.model.SettingsData.General import de.gematik.ti.erp.app.settings.repository.SettingsRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn val DATA_PROTECTION_LAST_UPDATED: Instant = - LocalDate.parse(BuildKonfig.DATA_PROTECTION_LAST_UPDATED).atStartOfDay().toInstant(ZoneOffset.UTC) + LocalDate.parse(BuildKonfig.DATA_PROTECTION_LAST_UPDATED).atStartOfDayIn(TimeZone.UTC) class SettingsUseCase( private val context: Context, @@ -61,9 +63,6 @@ class SettingsUseCase( val showMainScreenTooltip: Flow = settingsRepository.general.map { !it.mainScreenTooltipsShown } - var showDataTermsUpdate: Flow = - settingsRepository.general.map { it.dataProtectionVersionAcceptedOn < DATA_PROTECTION_LAST_UPDATED } - suspend fun welcomeDrawerShown() { settingsRepository.saveWelcomeDrawerShown() } @@ -73,7 +72,7 @@ class SettingsUseCase( suspend fun onboardingSucceeded( authenticationMode: SettingsData.AuthenticationMode, defaultProfileName: String, - now: Instant = Instant.now() + now: Instant = Clock.System.now() ) { sanitizedProfileName(defaultProfileName)?.also { name -> settingsRepository.saveOnboardingSucceededData(authenticationMode, name, now) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/TroubleshootingContent.kt b/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt similarity index 73% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/TroubleshootingContent.kt rename to android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt index 4b09b989..bf05057e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/TroubleshootingContent.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt @@ -16,10 +16,11 @@ * */ -package de.gematik.ti.erp.app.cardwall.ui +package de.gematik.ti.erp.app.troubleShooting import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth @@ -45,22 +46,87 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +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.settings.ui.buildFeedbackBodyWithDeviceInfo import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText +import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.PrimaryButton import de.gematik.ti.erp.app.utils.compose.SecondaryButton import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedLinkString import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +object TroubleShootingNavigation { + object TroubleshootingPageA : Route("TroubleshootingPageA") + object TroubleshootingPageB : Route("TroubleshootingPageB") + object TroubleshootingPageC : Route("TroubleshootingPageC") + object TroubleshootingNoSuccessPage : Route("TroubleshootingNoSuccessPage") +} + +@Composable +fun TroubleShootingScreen( + onClickTryMe: () -> Unit, + onCancel: () -> Unit +) { + val navController = rememberNavController() + + NavHost( + navController, + startDestination = TroubleShootingNavigation.TroubleshootingPageA.path() + ) { + composable(TroubleShootingNavigation.TroubleshootingPageA.route) { + NavigationAnimation { + TroubleshootingPageAContent( + onClickTryMe = onClickTryMe, + onNext = { navController.navigate(TroubleShootingNavigation.TroubleshootingPageB.path()) }, + onBack = onCancel + ) + } + } + composable(TroubleShootingNavigation.TroubleshootingPageB.route) { + NavigationAnimation { + TroubleshootingPageBContent( + onClickTryMe = onClickTryMe, + onNext = { navController.navigate(TroubleShootingNavigation.TroubleshootingPageC.path()) }, + onBack = { navController.popBackStack() } + ) + } + } + composable(TroubleShootingNavigation.TroubleshootingPageC.route) { + NavigationAnimation { + TroubleshootingPageCContent( + onClickTryMe = onClickTryMe, + onNext = { navController.navigate(TroubleShootingNavigation.TroubleshootingNoSuccessPage.path()) }, + onBack = { navController.popBackStack() } + ) + } + } + composable(TroubleShootingNavigation.TroubleshootingNoSuccessPage.route) { + NavigationAnimation { + TroubleshootingNoSuccessPageContent( + onNext = onCancel, + onBack = { navController.popBackStack() } + ) + } + } + } +} + @Composable fun TroubleshootingPageAContent( onBack: () -> Unit, @@ -324,3 +390,33 @@ private fun TroubleshootingScaffold( } } } + +@Composable +fun TroubleshootingInfo( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Text( + stringResource(R.string.cdw_enter_troubleshooting_title), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + Text( + stringResource(R.string.cdw_enter_troubleshooting_subtitle), + style = AppTheme.typography.body2, + textAlign = TextAlign.Center + ) + SpacerMedium() + Button( + onClick = onClick, + shape = RoundedCornerShape(8.dp), + elevation = ButtonDefaults.elevation(defaultElevation = 0.dp), + contentPadding = PaddingValues(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny) + ) { + Icon(Icons.Outlined.Lightbulb, null) + SpacerTiny() + Text(stringResource(R.string.cdw_enter_troubleshooting_action)) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt index f3ce1d46..0baaafec 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt @@ -18,52 +18,66 @@ package de.gematik.ti.erp.app.utils -import java.time.Instant +import android.os.Build +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.toJavaYear +import de.gematik.ti.erp.app.fhir.parser.toJavaYearMonth +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toJavaLocalTime +import kotlinx.datetime.toLocalDateTime import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.Year import java.time.YearMonth -import java.time.ZoneId -import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -import java.time.temporal.TemporalAccessor val dateTimeShortFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) fun dateTimeShortText(instant: Instant): String = - LocalDateTime - .ofInstant(instant, ZoneId.systemDefault()) + instant.toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime() .format(dateTimeShortFormatter) val dateTimeMediumFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) -fun dateTimeMediumText(instant: Instant, zoneId: ZoneId = ZoneOffset.UTC): String = - LocalDateTime - .ofInstant(instant, zoneId) +fun dateTimeMediumText(instant: Instant, zone: TimeZone = TimeZone.currentSystemDefault()): String = + instant.toLocalDateTime(zone) + .toJavaLocalDateTime() .format(dateTimeMediumFormatter) private val YearMonthPattern = DateTimeFormatter.ofPattern("MMMM yyyy") private val MonthPattern = DateTimeFormatter.ofPattern("yyyy") -fun temporalText(temporalAccessor: TemporalAccessor, zoneId: ZoneId = ZoneOffset.UTC): String = - when (temporalAccessor) { - is Instant -> - LocalDateTime - .ofInstant(temporalAccessor, zoneId) - .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) - is LocalDate -> - temporalAccessor - .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) - is YearMonth -> - temporalAccessor - .format(YearMonthPattern) - is Year -> - temporalAccessor - .format(MonthPattern) - is LocalTime -> - temporalAccessor - .format(DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)) +fun temporalText(temporal: FhirTemporal, timeZone: TimeZone = TimeZone.UTC): String = + when (temporal) { + is FhirTemporal.Instant -> temporal.value.toLocalDateTime(timeZone).toJavaLocalDateTime() + .format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) + + is FhirTemporal.LocalDate -> temporal.value.toJavaLocalDate() + .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) + + is FhirTemporal.LocalDateTime -> temporal.value.toJavaLocalDateTime() + .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) + + is FhirTemporal.LocalTime -> temporal.value.toJavaLocalTime() + .format(DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)) + + is FhirTemporal.Year -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + temporal.value.toJavaYear().format(MonthPattern) + } else { + error("VERSION.SDK_INT < O") + } + + is FhirTemporal.YearMonth -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + temporal.value.toJavaYearMonth().format(YearMonthPattern) + } else { + error("VERSION.SDK_INT < O") + } + else -> "n.a." } diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt index 0ad80c74..9b6bf487 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt @@ -26,11 +26,14 @@ import android.content.pm.ResolveInfo import android.content.res.Resources import android.net.Uri import android.text.format.DateFormat +import android.widget.Toast import androidx.activity.addCallback import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -72,6 +75,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.KeyboardArrowRight import androidx.compose.material.icons.rounded.Undo import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -86,9 +90,11 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -120,21 +126,16 @@ import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import io.github.aakira.napier.Napier -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Date -@Composable -fun SpacerMaxWidth() = - Spacer(modifier = Modifier.fillMaxWidth()) - -@Composable -fun Spacer40() = - Spacer(modifier = Modifier.size(40.dp)) - @Composable fun Spacer32() = Spacer(modifier = Modifier.size(32.dp)) @@ -271,6 +272,9 @@ fun NavigationClose(modifier: Modifier = Modifier, onClick: () -> Unit) { } } +fun String.toAnnotatedString() = + buildAnnotatedString { append(this@toAnnotatedString) } + @Composable fun annotatedLinkString(uri: String, text: String, tag: String = "URL"): AnnotatedString = buildAnnotatedString { @@ -831,6 +835,14 @@ fun phrasedDateString(date: LocalDateTime): String { return "${date.format(dateFormatter)} $at ${timeFormatter.format(timeOfDate)}" } +fun dateString(date: kotlinx.datetime.LocalDateTime): String { + return dateString(date.toJavaLocalDateTime()) +} + +fun timeString(time: kotlinx.datetime.LocalDateTime): String { + return timeString(time.toJavaLocalDateTime()) +} + fun dateString(date: LocalDateTime): String { val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) return date.format(dateFormatter) @@ -848,7 +860,8 @@ fun timeString(date: LocalDateTime): String { fun dateWithIntroductionString(@StringRes id: Int, instant: Instant): String { val dateFormatter = remember { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) } val date = remember { - instant.atZone(ZoneId.systemDefault()) + instant.toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime() .toLocalDate().format(dateFormatter) } val combinedString = annotatedStringResource(id, date).toString() @@ -875,3 +888,91 @@ fun LabeledText(description: String, content: String?) { fun LabeledText(descriptionResource: Int, content: String?) { LabeledText(stringResource(descriptionResource), content) } + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun Label( + modifier: Modifier = Modifier, + text: String?, + label: String? = null, + onClick: (() -> Unit)? = null +) { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + + val verticalPadding = if (label != null) { + PaddingDefaults.ShortMedium + } else { + PaddingDefaults.Medium + } + + val noValueText = stringResource(R.string.pres_details_no_value) + + Row( + modifier = modifier + .combinedClickable( + onClick = { + onClick?.invoke() + }, + onLongClick = { + if (text != null) { + clipboardManager.setText(AnnotatedString(text)) + Toast + .makeText(context, "$label $text", Toast.LENGTH_SHORT) + .show() + } + }, + role = Role.Button + ) + .padding(horizontal = PaddingDefaults.Medium, vertical = verticalPadding) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column(Modifier.weight(1f)) { + Text( + text = text ?: noValueText, + style = AppTheme.typography.body1 + ) + if (label != null) { + Text( + text = label, + style = AppTheme.typography.body2l + ) + } + } + if (onClick != null) { + SpacerMedium() + Icon(Icons.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.neutral400) + } + } +} + +@Composable +fun HealthPortalLink( + modifier: Modifier +) { + Column(modifier = modifier) { + Text( + text = stringResource(R.string.pres_detail_health_portal_description), + style = AppTheme.typography.body2l + ) + + val linkInfo = stringResource(R.string.pres_detail_health_portal_description_url_info) + val link = stringResource(R.string.pres_detail_health_portal_description_url) + val uriHandler = LocalUriHandler.current + val annotatedLink = annotatedLinkStringLight(link, linkInfo) + + SpacerSmall() + ClickableText( + text = annotatedLink, + onClick = { + annotatedLink + .getStringAnnotations("URL", it, it) + .firstOrNull()?.let { stringAnnotation -> + uriHandler.openUri(stringAnnotation.item) + } + }, + modifier = Modifier.align(Alignment.End) + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt index 78fec14d..4009df23 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt @@ -27,12 +27,15 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.R -import java.time.Duration -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import kotlin.time.Duration enum class TimeDiff { FewMinutes, @@ -62,9 +65,9 @@ fun timeDescription( val dt by rememberUpdatedState(instant) val fmt by rememberUpdatedState(formatter) val timeString = remember(dt, fmt) { - val duration = Duration.between(dt, Instant.now()) - val diffMinutes = duration.toMinutes() - val localDt = LocalDateTime.ofInstant(dt, ZoneId.systemDefault()) + val duration = Clock.System.now() - dt + val diffMinutes = duration.inWholeMinutes + val localDt = dt.toLocalDateTime(TimeZone.currentSystemDefault()) mutableStateOf(fmt(timeDiff(diffMinutes = diffMinutes), localDt, duration)) } return timeString @@ -84,8 +87,8 @@ object TimeDescriptionDefaults { val fmt: TimeDescriptionFormatter = { diff, localDt, _ -> when (diff) { TimeDiff.FewMinutes -> fewMinutes - TimeDiff.Today -> today.format(hours.format(localDt)) - TimeDiff.Other -> other.format(localDt) + TimeDiff.Today -> today.format(localDt.toJavaLocalDateTime().format(hours)) + TimeDiff.Other -> localDt.toJavaLocalDateTime().format(other) } } fmt diff --git a/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt b/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt index df81f1ae..e35659ac 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt @@ -32,12 +32,13 @@ import de.gematik.ti.erp.app.vau.usecase.TrustedTruststoreProvider import de.gematik.ti.erp.app.vau.usecase.TruststoreConfig import de.gematik.ti.erp.app.vau.usecase.TruststoreTimeSourceProvider import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder import org.kodein.di.DI import org.kodein.di.bindSingleton import org.kodein.di.instance +import kotlin.time.Duration val vauModule = DI.Module("vauModule") { bindSingleton { @@ -57,7 +58,7 @@ val vauModule = DI.Module("vauModule") { networkSecPrefs = instance(NetworkSecurePreferencesTag) ) } - bindSingleton { { Instant.now() } } + bindSingleton { { Clock.System.now() } } bindSingleton { { untrustedOCSPList: UntrustedOCSPList, untrustedCertList: UntrustedCertList, diff --git a/android/src/main/res/drawable-xhdpi/ic_info.webp b/android/src/main/res/drawable-xhdpi/ic_info.webp deleted file mode 100644 index f91593ef7dd9858084f87ca2331c9fcc9ad7b654..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21770 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 diff --git a/android/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_small.webp b/android/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_small.webp deleted file mode 100644 index d0e1bd9d98e2539e432d80088ec8b6a5c44a22e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1274 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 diff --git a/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_small.webp b/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_small.webp deleted file mode 100644 index 1b6ba9739bbdebe93aa59d6894948f7a16fc275f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1648 zcmWIYbaTsKV_*n(bqWXzu!!JdU|=u+Vrww+baoDqU;=U(m?S_X3s5RIucRo*-AN%L zGD-m?1_O!7#RV`n7MhWP;oCI^Aj!aRSr>~uAtBC`)Z%2Iq7Oh^QUtV|fq^jv$QDV5 zuq#09BnZ0)#4ZX6at5kd0Ay>VBe9c^*ySY!B|tSNKIgA?khp~qQ(3^k zz~92aup2H^_~3|!GLha&VM;sS?X6@YqVfd0$} z!gPilhExUx22X}OhGYgkAj=4tNDLXwfGNgD3uvdl^#MKs{l_aNs<^*e@vJ{F^VhuV zkt}?-+$Jq4GOg$F{=EBj;RUlvDpObgOpcg!Kepd-+r(horuhfcRkyv)+W)@l+5G?Y zJLVbgUi~Dke8#V{lls^=;*t9Z#H=(iZsW?tjxGlm6Vk#q09rexu6D zLgz--7bn7Wo?I1&1qqNAyJp6JPM<^Z$)hh7>6MId8Kd+ve@?{~0VfVc7a!ZZF;^bz zYR}~9eER>nXZ>w2yZpdUpDfrbr`_DA;*zoMN!7FP&HfyUKj-f4Df8=)vyDHq$!$}2 z%2k6b!^xQ;CEZ$kZ&?X?$NK(Hl5Tl%ZS57uUr&uv8=aj#WjtFIl*4=U^Dg;g^)HUQ z8+Fajx7GhNOFp2$%Qb1IyGh?;i@wJn<~Dr1*1brhYtf3|4W>mWlf*n{)+=cVeNyaF zay{Fw!yC3)2dFS?lIzV7@$4g79Fximcm0{Vp>5SZ2L(Nb93_We9j%EHZBTT@Md96p zkMgN+a?ED`pL(EICv1>-J}x^DuO>vVYzVP4!KiKG&aLcKPL( z?XP#OgQ1|Nd&v^#CtW&fHf=-vy4OH2g3)4?xW&)IrmgyKG524Q%%G&fa!lq#Ov{|L42q({u4kys+Wq)x zg@r_q&*b8Bme1!D=gmr6o@O%3ztEZg>+W+I7B9ZL%_%-6k=$c=u5xwMYH8lbT!$-) z8lJh_e*e9#$8ed|*`lRUtIcPu?sy2ojwiudByYj#!7N{%gV6e(#j zI`*oy>B|JRBxS}8kM#Q{X9g+69P2qEl4-IpVJZKM#;+wt;(}W10w>%q+8TA-{MsUm z+naqto?Z=(eA2ovdE*z~gJ!AKD&FgF*SenAT6hxiyJve4wIeRuhZB~KP+?G%EMHhWs!sOZgc5hjtzbtd>m*0L?Yxi6@z-s>a zj;YZG=PxejD!rrjo_MPJbibnR+$Z0Q?oBfDH%ckn6302ctk3_+?k6`7KFhks!vFxa CR9DOZ diff --git a/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_small.webp b/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_small.webp deleted file mode 100644 index 055e0d421c00a9f48a7b3b1e8692c81c55b4cea7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3874 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 bYF}1d#==7ul3=-UlS9Y_v-KtMeE|3m$d9J* diff --git a/android/src/main/res/drawable-xxhdpi/pharmacist_circle_red.webp b/android/src/main/res/drawable-xxhdpi/pharmacist_circle_red.webp deleted file mode 100644 index 8eb6a51b0e39c22ef8de279e679003339c9628b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40246 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 SC;Q9!lKGN%V&}`2_gn?wGV8+t diff --git a/android/src/main/res/drawable-xxxhdpi/ic_info.webp b/android/src/main/res/drawable-xxxhdpi/ic_info.webp deleted file mode 100644 index 59a5b2d19ac6ff6383554906e14b628a12ef866d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72884 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

f^nd{3 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 K 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�=V8n$T? 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�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 HKjcdQ`2cpu diff --git a/android/src/main/res/values-ar/strings.xml b/android/src/main/res/values-ar/strings.xml index a611a7a3..c1f9f8d6 100644 --- a/android/src/main/res/values-ar/strings.xml +++ b/android/src/main/res/values-ar/strings.xml @@ -6,9 +6,6 @@ تراجع الساعة رقمي. سريع. آمن. - إضافة وصفات طبية - هل تلقيت نسخة مطبوعة من وصفة طبية؟ يمكنك إضافة وصفات إلى التطبيق عن طريق مسح كود الوصفة الطبية المطلوبة. - مفهوم رقم الطلبية كود الدخول شروط الاستخدام @@ -20,12 +17,12 @@ وبالتالي فهو كود وصفة غير سارٍ تم مسح هذا الرمز للوصفة من قبل - تم التعرف على %s وصفة - تم التعرف على %s وصفات + + تم التعرف على %s وصفة - . - + + تم التعرف على %s وصفات إلغاء ضوء الكاميرا @@ -34,18 +31,18 @@ المواصلة ابدأ الآن ما تحتاج إليه: - إدخال رقم الدخول + أدخل رقم الوصول للبطاقة إدخال رقم التعريف الشخصي جرب مرة أخرى فشل الاتصال بالخادم. تم إدخال رقم تعريف شخصي خاطيء. - لديك عدد %s محاولة أخرى قبل وقف البطاقة. - لديك عدد %s محاولات أخرى قبل وقف البطاقة. + + لديك %s محاولة أخرى قبل وقف البطاقة. - . - + + لديك %s محاولات أخرى قبل وقف البطاقة. تم إدخال CAN خاطيء تجد رقم تسجيل الدخول أعلى يمينًا في بطاقتك الصحية. @@ -64,15 +61,6 @@ النسخة: %s Build-Hash: %s قائمة التنقيح - كود الوصفة الطبية - قم بمسح كود الوصفة الطبية في صيدليتك. - يحتوي هذا الكود الجماعي على %s وصفات طبية - الصرف في الصيدلية - أنت توجد في صيدلية وتريد صرف وصفتك الطبية. - اطلب أو احجز - أرسل وصفتك الطبية إلى الصيدلية وقرر الطريقة التي ترغب بها في تلقي الدواء. - حدد موقعك وابحث عن الصيدليات في منطقتك - إتاحة المقر مفتوح حتى الساعة %s مفتوح دائمًا هيئة التحرير @@ -125,23 +113,14 @@ هل ترغب في حذف هذه الوصفة بشكل دائم؟ حذف إلغاء - يُسمح بالمستحضرات الطبية البديلة. نظرا للمتطلبات القانونية للتأمين الصحي الخاص بك، يمكن أن تسليمك بديل. - احجز بشكل مُلزم - اطلب خدمة المراسلة - اطلب خدمة التوصيل - يرجى الانتباه إلى أنه قد يتم تطبيق رسوم إضافية مقابل الأدوية الموصوفة أيضًا. أوقات العمل الموقع الإلكتروني - هل ترغب في صرف الوصفات الطبية في %s بشكل ملزم؟ قابلة للصرف اليوم فقط كدافع ذاتي التسجيل تفعيل وظيفة NFC يُرجى تفعيل وظيفة NFC بجهازك لتتمكن من تسجيل الدخول باستخدام بطاقتك الصحية. تفعيل التصويب - العرض في شكل كود فردي - العرض في شكل كود جماعي - %s من %s تم صرف الوصفات الطبية؟ هل ترغب في تحديد الوصفات باعتبارها تم صرفها؟ لم يتم الصرف @@ -169,7 +148,6 @@ الاتصال بالخط الفني الساخن المشاركة في الاستبيان +49 800 277 377 7 - معرفة المزيد أرغب في المساعدة في تحسين هذا التطبيق. ويتضمن ذلك معلومات عن الأجهزة والبرامج الموجودة على هاتفك، وإعدادات تطبيق الوصفات الطبية الإلكترونية وحجم الاستخدام، ولكنه لا يتضمن مطلقًا بيانات حول شخصك أو حالتك الصحية. وتُتاح البيانات فقط لشركة gematik GmbH بواسطة معالجي البيانات وتُحذف بعد 180 يومًا على أقصى تقدير. كما يمكنك إلغاء تفعيل التحليل في أي وقت من قائمة التطبيق. @@ -177,12 +155,8 @@ تحسين التطبيق يظل التحليل دون إفصاح عن الهوية غير مفعل %s شكرًا لك على المساعدة! - ملاحظة - قد يحدث بعض التأخير حتى إظهار الوصفات الطبية التي تم صرفها في قسم \"الأرشيف\". - موافق التسجيل يُرجى تحديد هويتك لتنزيل الوصفات. - تم صرفها في:%s ملاحظة للصيدليات: يحصل هذا التطبيق على تفاصيل الاتصال والمعلومات حول الصيدليات من الموقعmein-apothekenportal.de التابع لاتحاد الصيدليات الألماني ج.م. هل اكتشفت خطأ أو ترغب في تصحيح البيانات؟ معرفة المزيد الصيدليات @@ -193,7 +167,6 @@ وسائل المساعدة في الاستخدام التكبير يتيح تكبير حجم التطبيق عبر ضم أو سحب الأصابع (الشد للتكبير). - ملاحظة كلمة السر قم بتأمين بياناتك بكلمة سر من اختيارك. كلمة السر @@ -203,8 +176,8 @@ التوصيات:%s كتابة بريد إلكتروني أثناء إرسال رسالتك سيتم نقل المعلومات التالية عبر الجهاز ونظام التشغيل المُستخدم: - يمكن الصرف قريبًا - لا يمكن لهذه الصيدلية استقبال الوصفات الإلكترونية في الوقت الحالي. + استرداد في الموقع فقط + لا يمكنك بعد إرسال الوصفات الطبية الإلكترونية إلى هذه الصيدلية. مفتوح حديثًا خدمة المراسلة إرسال @@ -229,10 +202,6 @@ سارٍ لمدة %s أيام - فتح الماسح الضوئي - نقوم بمعالجة معلومات جهازك!\nيستخدم هذا التطبيق مجموعة ML Kit من جوجل لقراءة رمز الوصفة الطبية. إذا اخترت \"قبول\" ، فأنت توافق على أنه يجوز لشركة جوجل من وقت لآخر الوصول إلى معلومات الجهاز ومعالجتها لغرض تحليل الاستخدام والتشخيصات وإعداد ML Kit. ويحق لك إلغاء موافقتك في أي وقت دون التأثير على قانونية المعالجة التي تمت معالجتها في وقت سابق. ومع ذلك، سيؤدي الرفض إلى عدم القدرة على استخدام الماسح الضوئي لرمز الوصفة الطبية. - موافق - إلغاء خطأ 20 10 76631 شهادة بطاقتك الصحية غير صالحة. هل ربما انتهت صلاحية بطاقتك؟ يُرجى الاتصال بشركة التأمين الصحي الخاصة بك. محاولات تسجيل دخول غير ناجحة @@ -278,20 +247,11 @@ يُرجى إدخال اسم للبروفايل الجديد. اسم البروفايل الصفحات الشخصية - إضافة بروفايل - بطاقة صحية - التواصل مع شركة التأمين الصحي - لكي تتمكن من التسجيل في هذا التطبيق، أنت بحاجة إلى بطاقة صحية تعمل بنظام الاتصال قريب المدى وكذلك رقم التعريف الشخصي الخاص بها. - سوف تحصل عليها مجانا من شركة التأمين الصحي لك. ويجب عليك إثبات هويتك بوثيقة هوية رسمية. هكذا يمكنك التعرف على البطاقة الصحية ذات الاتصال قريب المدى - اختر التأمين الصحي - لم يتم الاختيار - ما الذي ترغب في طلبه؟ لا يمكن الاتصال عبر هذا التطبيق يُرجى استخدام القنوات المعتادة للتواصل مع التأمين الخاص بك. بطاقة صحية و رمز PIN رمز PIN فقط - تواصل مع شركة التأمين الصحي الخاصة بك التسجيل في تطبيق الوصفة الإلكترونية لا يجوز ان يكون هذا الحقل فارغًا. يوجد بروفايل بالفعل بنفس الاسم المذكور. @@ -323,8 +283,6 @@ تغير هذا منذ %s: ماذا يحدث عندما تفتح التطبيق؟ ماذا يحدث عندما أستخدم خاية الكاميرا / اقرأ الوصفات الطبية بالكاميرا؟ - اختر الصفحة الشخصية - معالجة الصفحات الشخصية لا توجد وصفات طبية جديدة @@ -338,7 +296,6 @@ قيد الصرف تم الصرف غير معروف - التفاصيل عرض بروتوكول الدخول يمكنك أن ترى هنا من وصل إلى وصفاتك الطبية المقصود به هو مفتاح دخول لخدمة الوصفات الطبية @@ -375,7 +332,7 @@ كيف تريد المواصلة؟ اطلب متوفر في القريب العاجل - احجز للاستلام الآن أو التسليم عبر خدمة التوصيل أو خدمة التسليم بالبريد + احجز الآن للتحصيل أو احصل عليه عن طريق خدمة البريد السريع أو الشحن الحفظ للطلب لاحقًا حفظ الوصفات الطبية على الجهاز @@ -389,9 +346,6 @@ فضل الارتباط بالبطاقة الصحية بروفايلك الحالي مرتبط بالفعل ببطاقة صحية أخرى (رقم تأمين صحي %s). بطاقتك الصحية مرتبطة بالفعل ببروفايل آخر. يُرجى تغيير البروفايل %s. - طلبي - احجز الآن - اطلب الآن حفظ بيانات الاتصال والعنوان الاتصال @@ -407,21 +361,13 @@ الرمز البريدي والمكان يُرجى إدخال الرمز البريدي والمكان من أجل التواصل معك. إرشادات التسليم (اختياري) - سيتم إرسال الوصفات الطبية الخاصة بك إلى هذه الصيدلية. لن تتمكن بعد ذلك من صرفها في أي صيدلية أخرى. - بيانات الاتصال وعنوان التسليم - الوصفات الطبية - نحتاج إلى بيانات الاتصال الخاصة بك من أجل الحصول على المشورة عن طريق الصيدلية وإبلاغك بحالة طلبك في الوقت الراهن. - قم بإدخال بيانات الاتصال نحتاج إلى المزيد من بيانات الاتصال - تم الطلب بنجاح - ستتصل بك الصيدلية الخاصة بك في أقرب وقت. - إغلاق حذف التغييرات؟ الحذف للقيام بعملية البحث يستخدم دليل الصيدليات الإحداثيات الجغرافية التي تم تحديدها بمساعدة OpenStreetMap (خريطة الشارع المفتوحة). نشكر المشروع على هذه المساعدة. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - الاستخدام وحماية البيانات + الخصوصية والاستخدام متابعة لقد تلقيت رقم التعريف الشخصي الخاص بك في خطاب من شركة التأمين الصحي الخاصة بك. تم استلام رمز PIN @@ -470,25 +416,11 @@ الأجهزة المتصلة مسجل منذ %s (هذا الجهاز) مسجل منذ %s - حديث - أرشيف - الصرف مرة أخرى؟ - ملحوظة: الصيدلية الأولى التي تقبل الوصفة الطبية تمنع معالجتها من قبل صيدلية أخرى. - إلغاء - موافق - - - أرسلت الوصفة %s من قبل إلى الصيدلية. صرفها مع ذلك من جديد؟ - - - - أرسلت بعض هذه الوصفات %s من قبل إلى الصيدلية.هل ترغب مع ذلكفي إرسال وصفات أخرى؟ - لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. رمز PIN أدخل رقم التعريف الشخصي (البطاقة الصحية). متابعة - المصادقة + تسجيل الدخول الأجهزة المتصلة حذف الجهاز؟ إلغاء @@ -510,8 +442,6 @@ هل تحتاج إلى المساعدة؟ لقد قمنا بجمع بعض النصائح لك لحل المشكلات الأكثر شيوعًا. ابدأ نصائح الاتصال - تم المسح الضوئي في: %s - الوصفة الطبية التي تم مسحها إلغاء الحظر تم حظر البطاقة تم إدخال رقم تعريف شخصي خاطيء ثلاث مرات. لذلك تم حظر بطاقتك لأسباب أمنية. @@ -541,10 +471,9 @@ رقم التعريف الشخصي للبطاقة الصحية ليس لديك حتى الآن بطاقة صحية مزودة بتقنية الإتصال اللاسلكية ورقم تعريف شخصي؟ اطلب الآن - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) أو: قم بتسجيل الدخول باستخدام %s. تطبيق تأمينك الصحي - "تجد رقم الدخول إلى بطاقتك (رقم الوصول إلى البطاقة - المعروف اختصارًا باسم CAN) في الركن الأيمن العلوي من مقدمة بطاقة التأمين الصحي الخاصة بك. " + "تجد رقم الدخول إلى بطاقتك في الركن الأيمن العلوي من مقدمة بطاقة التأمين الصحي الخاصة بك. " لا تحتوي بطاقتي على رقم الدخول @@ -567,6 +496,10 @@ في الجزء السفلي في الوسط في الجزء السفلي يسارًا المساعدة + تم الإرسال قبل %s دقيقة + تم الإرسال في %s + تم الإرسال للتو + تم الإرسال الساعة %s لم يعد صالحًا التسجيل بالتطبيق اختر التأمين الصحي @@ -603,10 +536,9 @@ الاسم التأمين الصحي الرقم التأميني - رقم الدخول (CAN) + رقم الوصول للبطاقة التسجيل تسجيل الخروج - يجب عليك تسجيل الدخول لتلقي الوصفات تلقائيا. حفظ يتغيرون تعديل الصورة الشخصية @@ -651,15 +583,13 @@ الصيدلية حدد رقم التعريف الشخصي المطلوب تم حفظ رقم التعريف الشخصي المطلوب - لا يمكن حفظ رقم التعريف الشخصي المطلوب الوصفة الإلكترونية حاليا مفتوحة وقريبة مني مصنف بواسطة … ابدأ البحث - إلغاء - سيتم استبدالها لك + تم استبدالها من أجلك الاحالة المباشرة - الصرف + الصيدليات رقم الهاتف (اختيارى) البحث بالاسم أو العنوان لا توجد معلومات صيدلية صالحة @@ -676,10 +606,8 @@ التسجيل الملف الشخصي 1 قريب مني - ليس لديهم وصفات طبية قابلة للاسترداد يمكن استردادها لاحقًا قابل للاسترداد من %s - %s / %s تحسينات المنتج تحليل مجهول ساعدنا في جعل هذا التطبيق أفضل. يتم جمع جميع بيانات المستخدم بشكل مجهول ويتم استخدامها فقط لتحسين تجربة المستخدم. @@ -721,10 +649,8 @@ رقم جرعة تاريخ المسألة - noctu سيتم استبدال هذه الوصفة لك كجزء من العلاج. بدون بيان - لا توجد رسوم خدمة الطوارئ دفع اضافي الدواء مذكرات التسليم @@ -800,4 +726,116 @@ سجل الدخول مرة أخرى لتحديث الوصفات الخاصة بك. رقم العنصر النشط الفاعلية والوحدة + تم استرداد القيمة قبل %s دقيقة + استرداد في %s + استردت للتو + استرداد في الساعة %s + الطلب #٪ s + تم استبدال هذه الوصفة لك كجزء من العلاج. + رسوم خدمة الطوارئ + لا يمكن ملء هذه الوصفة في الليل في الصيدلية دون دفع رسوم خدمة الطوارئ الإضافية. + ابحث هنا + الإعدادات + مشاركة الموقع في الإعدادات. + قريب مني + عقد لتحرير الاسم. + أدخل الاسم الجديد لملف التعريف. + يجب عليك تسجيل الدخول لتلقي الوصفات الطبية الرقمية من عيادتك. + تلقي الوصفات رقميًا؟ + اسحب الشاشة لأسفل للتحديث. + لا توجد وصفات طبية + أضف الوصفات باستخدام زر + في الزاوية اليمنى العليا. + تسجيل الدخول + الوصفات الطبية المردودة + ربما لاحقا + تسجيل الدخول + تعديل الصورة الشخصية + الوصفات الطبية المردودة + أدخل الاسم + حفظ + طلبي + المستلم: في + الوصفات الطبية + الصيدلية + إرسال + للتغيير + التقط في الصيدلية + التسليم عن طريق البريد + التسليم عن طريق البريد + %s وصفات + الاسترداد غير ممكن + لا يمكن استبدال وصفة طبية واحدة أو أكثر. + لم يتم تحديد وصفة + لاسترداد الوصفات ، يجب تحديد وصفة واحدة على الأقل. + أضف معلومات الاتصال + للتغيير + بدون وصفة طبية + ليس لديك حاليا أي وصفات طبية قابلة للاسترداد + يلتقط + فتى التوصيل + إرسال + اختر الوصفات + انقر هنا لمسح الوصفات + اضغط مع الاستمرار لتعديل الأسماء + أضف المزيد من الملفات الشخصية ، على سبيل المثال لأطفالك أو والديك. + انقر فوق الشاشة لتخطي تلميح الأداة المعروض. + كيفية تخليص؟ + كيف تريد أن تتلقى أدويتك؟ + تخليص مباشرة + استبدال الدواء في الموقع + اطلب + حجز أو تسليمها + تم + كود جماعي + رموز مفردة + + + لديك وصفة طبية %s . + + + + لديك %s وصفات. + + اختر اختيارًا + كل الوصفات + ما هي الوصفات؟ + متابعة + متابعة + معرفة المزيد + ملاحظة + يستخدم هذا التطبيق برنامجًا من Google للتعرف على الرموز. + معرفة المزيد + حول الماسح رمز وصفة + ما هي البيانات التي يحتوي عليها كود الوصفة؟ + يحتوي رمز الوصفة على معرّف للوصفة فقط. يسمح ذلك بإيجاد الوصفة الطبية في خدمة الوصفات الطبية في شبكة الصحة الرقمية. لا يحتوي رمز الوصفة الطبية على أي بيانات عنك أو عن أدويتك. + إذن لا أحد يستطيع فعل أي شيء باستخدام كود الوصفة وحده؟ + صيح. يجب تنزيل بيانات الوصفات الطبية من خدمة الوصفات الطبية. هذا يتطلب تسجيل دخول آمن. + من يمكنه التسجيل في خدمة الوصفات الطبية؟ + التسجيل في خدمة الوصفات الطبية في الشبكة الصحية الرقمية ممكن للأشخاص المؤمن عليهم والصيدليات والممارسات الطبية والمستشفيات. + لماذا يستخدم تطبيق الوصفات الطبية الإلكترونية ميزات Google؟ + تقدم Google وظائف يمكن دمجها بسهولة في التطبيقات والتي يتم تطويرها وتحديثها باستمرار بواسطة Google. هذا يضمن أن الوظائف تعمل على العديد من الأجهزة الطرفية المختلفة ويمكن تشغيلها بأمان. يستخدم التطبيق ميزة لتحسين وظائف الكاميرا والمسح الضوئي لأجهزة Android (Google ML Kit). + كيف يعمل تحسين Google ML Kit Scan؟ + تساعد Google ML Kit على تحسين الصورة الملتقطة بواسطة الكاميرا بحيث يمكن قراءة أكواد الوصفات حتى في ظروف الإضاءة السيئة أو مع طرز الكاميرا القديمة. + هل سيتم نقل البيانات المتعلقة بالوصفة الطبية أو الأدوية الخاصة بي إلى Google؟ + رقم يتم حفظ رمز وصفة القراءة مباشرة في التطبيق. لن يتم تمريرها إلى Google. لا يتم تخزين بيانات الوصفات الطبية في الكود ، فقط في شبكة الصحة الرقمية. من هناك يتم إرسالها إلى التطبيق. ليس لدى Google حق الوصول إلى شبكة الصحة الرقمية. + ما البيانات التي تعالجها Google عند استخدام ML Kit؟ + تمتلك Google فقط إمكانية الوصول إلى المعلومات الفنية حول الجهاز النهائي المستخدم والاستخدام العام للوظيفة الإضافية (مثل معدل الخطأ ، وإعدادات الكاميرا) من أجل تسجيل هذا إحصائيًا وبالتالي تحسين الوظيفة الإضافية. عند الوصول ، تسجل Google عنوان IP الخاص بجهازك الطرفي مؤقتًا. لن تسجل Google معلومات عنك ومحتويات الوصفة. + هل استخدام Google ML Kit تطوعي؟ + نعم. ومع ذلك ، تم تضمين ML Kit في الماسح الضوئي لرمز الوصفة في إصدار Android من تطبيق الوصفات الطبية الإلكترونية. إذا كنت تستخدم الماسح الضوئي لرمز الوصفة على جهاز Android ، فسيتم أيضًا استخدام وظيفة ML Kit دائمًا. ومع ذلك ، يمكنك الاستغناء عن استخدام الماسح الضوئي لرمز الوصفة. يمكن أيضًا تحميل الوصفات الطبية الخاصة بك في التطبيق إذا قمت بالتسجيل في الشبكة الصحية الرقمية باستخدام البطاقة الصحية الإلكترونية أو عبر تطبيق التأمين الصحي الخاص بك. + هل يمكنني رؤية من شاهد وصفاتي؟ + نعم. يتم تسجيل الدخول إلى بياناتك بالكامل في شبكة الصحة الرقمية. في تطبيق الوصفات الطبية الإلكترونية ، يمكنك معرفة من وصل إلى بياناتك. + بمن يمكنني الاتصال إذا كانت لدي أسئلة حول التطبيق أو الوصفة الإلكترونية؟ + يمكنك العثور على معلومات مفصلة في إعلان حماية البيانات. + عدد العبوات المقررة + لا توجد وصفات طبية + لهذا تحتاج إلى وصفات طبية قابلة للاسترداد. + اختر التأمين الصحي + ابحث عن التأمين + إلغاء + ما الذي ترغب في طلبه؟ + بالنسبة لهذا التطبيق ، تحتاج إلى بطاقة ورقم التعريف الشخصي المرتبط بها. + كيف تريد الاتصال بشركة التأمين الخاصة بك؟ + تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية + تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية + إغلاق diff --git a/android/src/main/res/values-en/strings.xml b/android/src/main/res/values-en/strings.xml index b89f7ea6..f63afd44 100644 --- a/android/src/main/res/values-en/strings.xml +++ b/android/src/main/res/values-en/strings.xml @@ -6,9 +6,6 @@ Back at Digital. Fast. Secure. - Add prescriptions - Have you received a prescription printout? You add prescriptions to the app by scanning the respective prescription code. - Agreed Task ID Access code Terms of Use @@ -30,7 +27,7 @@ Continue Let\'s get started What you need: - Enter access number + Enter card access number Enter PIN Try again Failed to connect to the server. @@ -56,15 +53,6 @@ Version: %s Build hash: %s Debug menu - Prescription code - Have this prescription code scanned at your pharmacy. - This group code combines %s prescriptions - Redeem at pharmacy - You are in a pharmacy and want to redeem your prescription. - Order or reserve - Submit your prescription to a pharmacy and decide how you would like to receive your medication. - Share location and find pharmacies in your area - Share location Open until %s o\'clock Open continuously Imprint @@ -117,23 +105,14 @@ Do you want to permanently delete this prescription? Delete Cancel - Substitutes are permitted. You may be given an alternative due to the legal requirements of your health insurance. - Make a binding reservation - Request delivery service - Delivery by mail order - Please note that prescribed medication may also be subject to additional payments. Opening hours Website - Redeem the following prescriptions with binding effect at %s? Can only be redeemed today as self-paying customer Log in Enable NFC Please enable the NFC function on your device to log in with your medical card. Enable Correct - Display as single codes - Display as group code - %s of %s Prescriptions redeemed? Would you like to mark the prescriptions as redeemed? Not redeemed @@ -161,7 +140,6 @@ Call technical hotline Take part in the survey +49 800 277 377 7 - Find out more I would like to help improve the app This comprises the hardware and software information of your phone, e-prescription app settings and the extent of use, but never your personal or health data. The data is provided exclusively by data processing providers to gematik GmbH and deleted after a maximum of 180 days. You can disable the analysis again at any time via the menu in the app. @@ -169,12 +147,8 @@ Improve app Anonymous analysis remains disabled %s Thank you for your support! - Note - There may be a delay before redeemed prescriptions are moved to the archive. - OK Log in Please identify yourself in order to download prescriptions. - Redeemed on %s Note to pharmacies: we obtain the contact details for and information about pharmacies from mein-apothekenportal.de provided by the Deutscher Apothekenverband e.V. Have you found an error or would you like to correct any data? Find out more Pharmacies @@ -185,7 +159,6 @@ Accessibility aids Zoom Enables the app to be zoomed in/out by moving fingers together or apart on the screen (pinch-to-zoom). - Note Password Secure your data with a password of your choice. Password @@ -195,10 +168,10 @@ Recommendations: %s Write email The following information about the hardware and operating system you use is transferred when you send an email: - Can be redeemed soon - This pharmacy is not yet able to receive any e-prescriptions. + Redeem on site only + You cannot yet send e-prescriptions to this pharmacy. Currently open - Delivery service + courier service Mail order Filter Filter @@ -213,10 +186,6 @@ Still valid for %s day Still valid for %s days - Open scanner - We process your device information!\nThis app uses the ML Kit from Google to read the prescription code. By clicking \"Accept\", you agree to Google occasionally accessing device information and processing it for the purpose of usage analysis, diagnostics and configuration of the ML Kit. You have the right to revoke your consent at any time, although this will not affect the lawfulness of any processing already performed. However, such a revocation will mean that the prescription code scanner cannot be used. - Agreed - Cancel Error 20 10 76631 Your medical card\'s certificate is invalid. Your card may have expired. Please contact your health insurance company. Unsuccessful login attempts @@ -258,20 +227,11 @@ Please enter a name for the new profile. Profile name Profiles - Add profile - Medical card - Contact health insurance company - You can use an NFC-enabled medical card and the associated PIN to log into this app. - You can obtain one free of charge from your health insurance company. You need to provide an official form of identification as proof of identity. How to identify an NFC-enabled medical card - Select health insurance company - No selection - What would you like to apply for? Contact is not possible via this app Please contact your health insurance company via the usual channels. Medical card & PIN PIN only - Contact your health insurance company Log in to e-prescription app The name field cannot be empty. A profile with this name already exists. @@ -303,18 +263,15 @@ This has changed since %s: What happens when you open the app? What happens if I use the camera function/read prescriptions using the camera? - Select profile - Edit profiles No new prescriptions available - %s prescription updated - %s prescriptions updated + %s new prescription + %s new prescriptions Can be redeemed Being redeemed Redeemed Unknown - Details Show access logs Here you can see who has accessed your prescriptions This relates to access keys for the prescription service @@ -351,7 +308,7 @@ How would you like to continue? Order Available soon - Reserve now for collection or for delivery by courier or mail order + Reserve now for collection or have it delivered by courier service or shipping Save to order later on Save prescriptions on device @@ -361,9 +318,6 @@ Failed to connect medical card The current profile is already connected to a different medical card (health insurance number %s). Your medical card is already connected to a different profile. Switch to profile %s. - My order - Reserve now - Order now Save Contact details and address Contact @@ -379,23 +333,15 @@ Postcode and town/city Please enter a postcode and town/city for contact purposes. Delivery instruction (optional) - Your prescription will be sent to this pharmacy. You will then not be able to redeem it in any other pharmacy. - Contact details and delivery address - Prescriptions - We need your contact details in order for the pharmacy to be able to advise you and let you know the current status of your order. - Enter contact details More contact details required - Order successfully sent. - Your pharmacy will contact you soon. - Close Discard changes? Discard Searches with the pharmacy directory use geocoordinates provided with the assistance of OpenStreetMap. We thank this project for their help. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Usage & Privacy Policy + Privacy & Use Next - You will have received your PIN in a letter from your health insurance company. + You received your PIN in a letter from your health insurance company. PIN not received PIN Check your connection to the Internet and your device\'s time/date setting. @@ -442,21 +388,11 @@ Connected devices Registered since %s (this device) Registered since %s - Current - Archive - Redeem again? - Note: The first pharmacy to accept a prescription blocks it for processing by another pharmacy. - Cancel - OK - - You have already sent prescription %s to a pharmacy. Are you sure you want to redeem it again? - You have already sent some of these prescriptions to a pharmacy. Are you sure you want to send them to another pharmacy? - For security reasons, the connection to the recipe server is terminated after 12 hours. To reconnect, you need a health card and PIN for each connection process. PIN Enter your PIN (medical card). Next - Authentication + log in Connected devices Remove device? Cancel @@ -478,8 +414,6 @@ Do you need any help? We have put a few tips together for you to solve the most common problems. Launch connection tips - Scanned on: %s - Scanned prescription Unlock Card blocked The PIN was entered incorrectly three times. Your card has therefore been blocked for security reasons. @@ -509,10 +443,9 @@ Medical card PIN Don\'t have an NFC-enabled medical card and PIN yet? Order now - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Or: Sign in with your %s. health insurance company app - "Your card access number (CAN) is located in the top right-hand corner on the front of your medical card." + "Your card access number is located in the top right-hand corner on the front of your medical card." My medical card has no access number You have %s more attempt before your card is blocked. @@ -531,6 +464,10 @@ in the lower central area in the lower left area Help + Sent %s minutes ago + Sent on %s + Sent just now + Sent at %s o\'clock No longer valid Log in with app Select insurance company @@ -567,10 +504,9 @@ Name Insurance Insurance number - Access number + Card access number Log in Log out - You need to be logged in to receive prescriptions automatically. Save Change Edit profile picture @@ -615,15 +551,13 @@ Pharmacy Select desired PIN Desired PIN saved - It is not possible to save the desired PIN E-prescription Currently open and near me Filter by … start search - Cancel - Will be redeemed for you + Was redeemed for you direct assignment - Redeem + pharmacies phone number (optional) Search for name or address No valid pharmacy information @@ -640,10 +574,8 @@ Log in profile 1 Close to me - They have no redeemable prescriptions Redeemable later Redeemable from %s - %s / %s product improvements Anonymous Analysis Help us make this app better. All user data is collected anonymously and is only used to improve the user experience. @@ -681,10 +613,8 @@ no dosage date of issue - noctu This prescription will be redeemed for you as part of a treatment. Not specified - No emergency service fee additional payment Medicine Delivery Notes @@ -760,4 +690,112 @@ Sign in again to update your recipes. active ingredient number potency and unity + Redeemed %s minutes ago + Redeemed on %s + Redeemed just now + Redeemed at %s o\'clock + orders + This prescription was redeemed for you as part of a treatment. + emergency service fee + This prescription cannot be filled at night in a pharmacy without the additional payment of an emergency service fee. + Search here + Settings + Share location in settings. + Close to me + Hold to edit the name. + Enter the new name for the profile. + You must be logged in to receive digital prescriptions from your practice. + Receive recipes digitally? + Drag the screen down to refresh. + No prescriptions + Add recipes using the + button in the top right corner. + log in + Redeemed prescriptions + Vielleicht später + log in + Edit profile picture + Redeemed prescriptions + Enter name + Save + My order + Recipient: in + Prescriptions + Pharmacy + Send + To change + Pick up at the pharmacy + Delivery by courier + Delivery by mail + %s Recipes + Redeem not possible + One or more prescriptions could not be redeemed. + No recipe selected + To redeem recipes, at least one recipe must be selected. + Add contact information + To change + No prescription + You currently have no redeemable prescriptions + pickup + delivery boy + Mail order + choose recipes + Tap here to scan recipes + Long press to edit names + Add more profiles, eg for your children or parents. + Click on the display to skip the displayed tool tip. + How to redeem? + How would you like to receive your medication? + Redeem directly + Redeem medication on site + Order + Reserve or have it delivered + Done + collective code + single codes + + You have %s prescription. + You have %s recipes. + + Make a selection + All recipes + Which recipes? + Next + Next + Find out more + Note + This app uses software from Google to recognize codes. + Find out more + About the recipe code scanner + What data does the recipe code contain? + The recipe code contains only an identifier of the recipe. This allows the prescription to be found on the prescription service in the digital health network. The prescription code does not contain any data about you or your medication. + So nobody can do anything with the recipe code alone? + Correct. The prescription data must be downloaded from the prescription service. This requires a secure login. + Who can register for the prescription service? + Registering with the prescription service in the digital health network is possible for insured persons, pharmacies, medical practices and hospitals. + Why does the e-prescription app use Google features? + Google offers functions that can be easily built into apps and that are constantly being developed and updated by Google. This ensures that the functions work on many different end devices and can be operated securely. The app uses a feature to improve camera and scanning functionality for Android devices (Google ML Kit). + How does Google ML Kit scan enhancement work? + Google ML Kit helps to optimize the image captured by a camera so that the recipe codes can be read even in poor lighting conditions or with older camera models. + Will data about the prescription or my medication be passed on to Google? + no The read recipe code is saved directly in the app. It will not be passed on to Google. The prescription data is not stored in the code, only in the digital health network. From there they are sent to the app. Google does not have access to the digital health network. + What data does Google process when using ML Kit? + Google only has access to technical information about the end device used and the general use of the additional function (e.g. error rate, camera settings) in order to record this statistically and thus improve the additional function. When you access, Google temporarily records the IP address of your end device. Information about you and the contents of the recipe will not be recorded by Google. + Is the use of Google ML Kit voluntary? + Yes. However, ML Kit is built into the recipe code scanner in the Android version of the e-prescription app. If you use the recipe code scanner on an Android device, the ML Kit function is also always used. However, you can do without using the recipe code scanner. Your prescriptions can also be loaded into the app if you register with the digital health network with the electronic health card or via your health insurance app. + Can I see who has viewed my recipes? + Yes. All access to your data is fully logged in the digital health network. In the e-prescription app you can see who has accessed your data. + Who can I contact if I have questions about the app or the e-prescription? + You can find detailed information in the data protection declaration. + Number of packs prescribed + No prescriptions + For this you need redeemable prescriptions. + Select insurance company + Look for insurance + Cancel + What would you like to apply for? + For this app you need a card and the associated PIN. + How would you like to contact your insurance company? + Your insurance company offers the following contact options + Your insurance company offers the following contact options + Close diff --git a/android/src/main/res/values-pl/strings.xml b/android/src/main/res/values-pl/strings.xml index 233e93c5..1ff90cb0 100644 --- a/android/src/main/res/values-pl/strings.xml +++ b/android/src/main/res/values-pl/strings.xml @@ -6,9 +6,6 @@ Powrót o Elektronicznie. Szybko. Bezpiecznie. - Dodaj recepty - Otrzymałeś(aś) wydruk recepty? Możesz dodać receptę do aplikacji, skanując jej kod. - Rozumiem ID zadania Kod dostępu Warunki korzystania @@ -32,7 +29,7 @@ Kontynuuj Zaczynamy Co jest potrzebne: - Wprowadź numer dostępu + Wprowadź numer dostępu do karty Wprowadź PIN Spróbuj ponownie Nie udało się utworzyć połączenia z serwerem. @@ -60,15 +57,6 @@ Wersja %s Build-Hash: %s Menu debugowania - Kod recepty - Zeskanuj ten kod recepty w swojej aptece. - Ten kod zbiorczy obejmuje %s recept(y). - Zrealizuj w aptece - Jesteś w aptece i chcesz zrealizować swoją receptę. - Zamów lub zarezerwuj - Wyślij swoją receptę do apteki i zdecyduj, w jaki sposób chcesz otrzymać swoje leki. - Zatwierdź lokalizację i znajdź aptekę w okolicy - Zatwierdź lokalizację Otwarta do godz. %s Otwarta całą dobę Stopka redakcyjna @@ -121,23 +109,14 @@ Czy chcesz nieodwołalnie usunąć tę receptę? Usuń Anuluj - Preparaty zastępcze są dozwolone. Ze względu na wytyczne ustawowe Twojej instytucji ubezpieczenia zdrowotnego możesz otrzymać alternatywę dla swojego leku. - Wiążąca rezerwacja - Zapytaj o usługę kurierską - Dostawa wysyłką - Także w przypadku leków na receptę mogą istnieć dopłaty. Godziny otwarcia Strona internetowa - Czy chcesz wiążąco zrealizować następujące recepty w %s? Możliwość zrealizowania jeszcze do dzisiaj jako płatnik indywidualny Zaloguj się Aktywuj NFC Aktywuj funkcję NFC w swoim urządzeniu, aby zalogować się za pomocą swojej karty zdrowia. Aktywuj Skoryguj - Wyświetl jako pojedyncze kody - Wyświetl jako kod zbiorczy - %s z %s Czy zrealizowano recepty? Czy chcesz zaznaczyć recepty jako zrealizowane? Niezrealizowane @@ -165,7 +144,6 @@ Zadzwoń na infolinię techniczną Weź udział w ankiecie +49 800 277 377 7 - Dowiedz się więcej Chcę pomóc w ulepszaniu tej aplikacji Obejmuje to informacje o sprzęcie i oprogramowaniu w Twoim telefonie, ustawienia aplikacji E-recepta oraz zakres korzystania. Nigdy nie gromadzimy danych dotyczących Twojej osoby ani Twojego zdrowia. Dane są udostępniane przez podmiot przetwarzający wyłącznie firmie gematik GmbH i są usuwane najpóźniej po 180 dniach. Użytkownik może w każdej chwili dezaktywować analizę w menu aplikacji. @@ -173,12 +151,8 @@ Optymalizacja aplikacji Anonimowa analiza pozostaje nieaktywna %s Dziękujemy za Twoje wsparcie! - Wskazówka - Recepty mogą zostać wyświetlone w archiwum z opóźnieniem. - OK Zaloguj się Przeprowadź identyfikację, aby pobrać recepty. - Zrealizowano dnia %s Wskazówka dla aptek:dane kontaktowe i informacje o aptekach pozyskujemy ze strony mein-apotkekenportal.de związku Deutscher Apothekenverband e.V. Znalazłeś(-aś) błąd lub chcesz skorygować dane? Dowiedz się więcej Apteki @@ -189,7 +163,6 @@ Pomoc w obsłudze Powiększ Umożliwia powiększenie aplikacji poprzez rozsuwanie lub zsuwanie palców (pinch-to-zoom). - Wskazówka Hasło Zabezpiecz swoje dane indywidualnym hasłem. Hasło @@ -199,8 +172,8 @@ Zalecenia: %s Napisz wiadomość e-mail Podczas wysyłania wiadomości przekazywane są następujące informacje na temat używanego sprzętu i systemu operacyjnego: - Zrealizowanie będzie możliwe wkrótce - Ta apteka nie może jeszcze przyjmować E-recept. + Zrealizuj tylko na miejscu + Nie możesz jeszcze wysyłać e-recept do tej apteki. Otwarta teraz Usługa kurierska Wysyłka @@ -221,10 +194,6 @@ - Otwórz skaner - Przetwarzamy dane Twojego urządzenia!\nDo odczytu kodu recepty aplikacja ta wykorzystuje ML Kit firmy Google. Klikając „Akceptuję”, zgadzasz się na to, aby firma Google od czasu do czasu miała dostęp do danych Twojego urządzenia i przetwarzała je do celów analizy korzystania, diagnostyki i konfiguracji ML Kit. Możesz w dowolnym momencie cofnąć swoją zgodę, co nie wpłynie na zgodność z prawem już przeprowadzonych operacji przetwarzania. Cofnięcie zgody będzie jednak skutkowało brakiem możliwości używania skanera kodów recept. - Wyrażam zgodę - Anuluj Błąd 20 10 76631 Certyfikat Twojej karty zdrowia jest nieważny. Być może termin ważności Twojej karty już upłynął. Skontaktuj się ze swoją kasą chorych. Bezskuteczne próby zalogowania się @@ -268,20 +237,11 @@ Podaj nazwę nowego profilu. Nazwa profilu Profile - Dodaj profil - Karta zdrowia - Skontaktuj się z instytucją ubezpieczenia zdrowotnego - Aby móc zalogować się w tej aplikacji, musisz posiadać kartę zdrowia obsługującą funkcję NFC i przypisany do niej kod PIN. - Otrzymasz ją bezpłatnie od swojej instytucji ubezpieczenia zdrowia. W tym celu musisz wylegitymować się za pomocą oficjalnego dokumentu tożsamości. W ten sposób rozpoznasz kartę zdrowia obsługującą funkcję NFC - Wybierz instytucję ubezpieczenia zdrowotnego - Brak wyboru - O co chcesz wnioskować? Kontakt za pośrednictwem tej aplikacji jest niemożliwy Skorzystaj ze standardowych kanałów, aby skontaktować się ze swoim ubezpieczycielem. Karta zdrowia i PIN Tylko PIN - Skontaktuj się ze swoją instytucją ubezpieczenia zdrowotnego Logowanie w aplikacji e-recepta Pole nazwy nie może być puste. Profil o podanej nazwie już istnieje. @@ -313,20 +273,17 @@ Zmieniło się to od %s: Co się stanie, kiedy otworzysz aplikację? Co się stanie, kiedy będę korzystać z funkcji kamery / odczytywać recepty za pomocą kamery? - Wybierz profil - Edytuj profile Brak nowych recept - Zaktualizowano %s receptę - Zaktaulizowano %s recepty - Zaktaulizowano %s recept + %s nowa recepta + %s nowe recepty + %s nowe recepty Gotowa do odbioru W realizacji Zrealizowana Nieznana - Szczegóły Wyświetl protokoły dostępu Tutaj możesz zobaczyć, kto miał dostęp do Twoich recept To jest kod dostępu do aplikacji E-recepta @@ -363,7 +320,7 @@ Jak chcesz kontynuować? Zamów Dostępne wkrótce - Zarezerwuj teraz w celu odbioru, wysyłki lub dostawy kurierem + Zarezerwuj teraz do odbioru lub zleć dostawę kurierem lub wysyłką Zapisz na potrzeby kolejnych zamówień Zapisz recepty na urządzeniu @@ -375,9 +332,6 @@ Połączenie karty zdrowia nie powiodło się Aktualny profil jest już powiązany z inną kartą zdrowia (numer ubezpieczenia zdrowotnego %s). Twoja karta zdrowia jest już powiązana z innym profilem. Zmień na profil %s. - Moje zamówienie - Zarezerwuj teraz - Zamów teraz Zapisz Dane kontaktowe i adres Kontakt @@ -393,23 +347,15 @@ Kod pocztowy i miejscowość Proszę podać kod pocztowy i miejscowość w celach kontaktowych. Wskazówki dotyczące dostawy (opcjonalnie) - Twoja recepta zostanie wysłana do tej apteki. Po wysłaniu jej zrealizowanie w innej aptece będzie niemożliwe. - Dane kontaktowe i adres dostawy - Recepty - Potrzebujemy Twoich danych kontaktowych, aby apteka mogła udzielić Ci porady oraz aby informować Cię o aktualnym statusie Twojego zamówienia. - Podaj dane kontaktowe Potrzebne są dodatkowe dane kontaktowe - Zamówienie zostało przekazane - Twoja apteka skontaktuje się z Tobą jak najszybciej. - Zamknij Odrzucić zmiany? Odrzuć Na potrzeby wyszukiwania wykaz aptek wykorzystuje współrzędne geograficzne, które zostały ustalone za pomocą OpenStreepMap. Dziękujemy temu projektowi za pomoc. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Warunki korzystania i ochrona danych + Prywatność i użytkowanie Dalej - Kod PIN otrzymałeś(aś) w liście od swojej instytucji ubezpieczenia zdrowotnego. + Otrzymałeś swój kod PIN w liście od swojej firmy ubezpieczeniowej. Nie otrzymałem(am) kodu PIN PIN Sprawdź połączenie z internetem oraz ustawienie godziny/daty na swoim urządzeniu. @@ -456,23 +402,11 @@ Podłączone urządzenia Zarejestrowane od %s (to urządzenie) Zarejestrowane od %s - Aktualne - Archiwum - Zrealizować ponownie? - Wskazówka: apteka, która jako pierwsza zaakceptowała receptę, blokuje ją, aby inna apteka nie mogła jej edytować. - Anuluj - OK - - Już wysłałeś(aś) receptę %s do apteki. Czy mimo to chcesz ją ponownie zrealizować? - Już wysłałeś(aś) jedną z tych recept do apteki. Czy mimo to chcesz wysłać ją do innych aptek? - - - Ze względów bezpieczeństwa połączenie z serwerem receptur zostaje zakończone po 12 godzinach. Aby ponownie nawiązać połączenie, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. PIN Wprowadź PIN (karty zdrowia) Dalej - Uwierzytelnienie + Zaloguj sie Podłączone urządzenia Usunąć urządzenie? Anuluj @@ -494,8 +428,6 @@ Potrzebujesz pomocy? Zebraliśmy kilka wskazówek, jak rozwiązać najczęściej występujące problemy. Wyświetl porady dotyczące połączenia - Zeskanowano dnia: %s - Zeskanowana recepta Odblokuj Karta została zablokowana Kod PIN został wprowadzony niepoprawnie trzy razy. Dlatego Twoja karta została zablokowana ze względów bezpieczeństwa. @@ -525,10 +457,9 @@ PIN do karty zdrowia Nie masz jeszcze karty zdrowia obsługującej funkcję NFC i kodu PIN? Zamów teraz - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Albo: zaloguj się za pomocą %s. Aplikacja Twojej instytucji ubezpieczenia zdrowotnego - "Numer dostępu (Card Access Number, w skrócie: CAN) znajdziesz w prawym górnym rogu swojej karty zdrowia." + "Numer dostępu znajdziesz w prawym górnym rogu swojej karty zdrowia." Moja karta nie ma numeru dostępu Masz jeszcze %s próbę, zanim Twoja karta zostanie zablokowana. @@ -549,6 +480,10 @@ na dole pośrodku na dole z lewej strony Pomoc + Wysłano %s minut temu + Wysłano dnia %s + Właśnie wysłano + Wysłano o godzinie %s Ważność upłynęła Zaloguj się za pomocą aplikacji Wybierz ubezpieczenie @@ -585,10 +520,9 @@ Imię i nazwisko Ubezpieczenie Numer ubezpieczonego - Numer dostępu (CAN) + Numer dostępu do karty Zaloguj się Wyloguj - Aby automatycznie otrzymywać recepty, musisz się zalogować. Zapisz Zmiana Edytuj zdjęcie profilowe @@ -633,15 +567,13 @@ Apteka Wybierz żądany kod PIN Zapisano żądany kod PIN - Nie można zapisać żądanego kodu PIN E-recepta Obecnie otwarte i blisko mnie Filtruj według … Zacznij szukać - Anuluj - Zostaną wykupione dla ciebie + Został odkupiony dla ciebie bezpośrednie przypisanie - Zrealizuj + apteki numer telefonu (opcjonalnie) Szukaj według nazwy lub adresu Brak prawidłowych informacji o aptece @@ -658,10 +590,8 @@ Zaloguj się profil 1 Blisko mnie - Nie mają recept zwrotnych Do wykorzystania później Do wykorzystania od %s - %s / %s ulepszenia produktu Anonimowa analiza Pomóż nam ulepszyć tę aplikację. Wszystkie dane użytkownika są zbierane anonimowo i służą wyłącznie do poprawy komfortu użytkowania. @@ -701,10 +631,8 @@ nie dawkowanie Data wydania - noctu - Ta recepta zostanie zrealizowana w ramach leczenia. + Ta recepta zostanie zrealizowana dla Ciebie w ramach leczenia. Brak danych - Brak opłaty za usługi ratunkowe dodatkowa opłata Lek Dokumenty dostawy @@ -780,4 +708,114 @@ Zaloguj się ponownie, aby zaktualizować swoje przepisy. numer składnika aktywnego moc i jedność + Wykorzystano %s minut temu + Wykorzystano %s + Odkupiony przed chwilą + Wykorzystane o godzinie %s . + Zamówienia + Ta recepta została zrealizowana dla Ciebie w ramach leczenia. + opłata za pogotowie + Recepty tej nie można zrealizować w aptece w nocy bez uiszczenia dodatkowej opłaty za pogotowie. + Szukaj tutaj + Ustawienia + Udostępnij lokalizację w ustawieniach. + Blisko mnie + Przytrzymaj, aby edytować nazwę. + Wprowadź nową nazwę profilu. + Musisz być zalogowany, aby otrzymywać cyfrowe recepty ze swojej praktyki. + Otrzymywać przepisy cyfrowo? + Przeciągnij ekran w dół, aby odświeżyć. + Brak recept + Dodaj przepisy za pomocą przycisku + w prawym górnym rogu. + Zaloguj sie + Realizowane recepty + Może później + Zaloguj sie + Edytuj zdjęcie profilowe + Realizowane recepty + Podaj nazwę + Zapisz + Moje zamówienie + Odbiorca: w + Recepty + Apteka + Wysłać + Zmienić + Odbierz w aptece + Dostawa kurierem + Dostawa pocztą + %s Przepisy + Odkupienie niemożliwe + Nie można zrealizować jednej lub więcej recept. + Nie wybrano przepisu + Aby wykorzystać przepisy, należy wybrać co najmniej jeden przepis. + Dodaj informacje kontaktowe + Zmienić + Bez recepty + Obecnie nie masz żadnych recept do zrealizowania + ulec poprawie + dostawca + Wysyłka + wybieraj przepisy + Kliknij tutaj, aby zeskanować przepisy + Naciśnij długo, aby edytować nazwy + Dodaj więcej profili, np. dla swoich dzieci lub rodziców. + Kliknij wyświetlacz, aby pominąć wyświetloną wskazówkę narzędzia. + Jak odkupić? + W jaki sposób chciałbyś otrzymywać leki? + Zrealizuj bezpośrednio + Zrealizuj leki na miejscu + Zamów + Zarezerwuj lub zamów dostawę + Gotowe + kodeks zbiorowy + pojedyncze kody + + Masz receptę na %s . + + + Masz %s przepisów. + + Wybierz + Wszystkie przepisy + Które przepisy? + Dalej + Dalej + Dowiedz się więcej + Wskazówka + Ta aplikacja używa oprogramowania Google do rozpoznawania kodów. + Dowiedz się więcej + O skanerze kodów receptur + Jakie dane zawiera kod receptury? + Kod receptury zawiera tylko identyfikator receptury. Dzięki temu receptę można znaleźć w serwisie recept w cyfrowej sieci zdrowia. Kod recepty nie zawiera żadnych danych o Tobie ani o Twoim leku. + Więc nikt nie może nic zrobić z samym kodem przepisu? + Prawidłowy. Dane recepty należy pobrać z serwisu recept. Wymaga to bezpiecznego logowania. + Kto może zarejestrować się w usłudze recepty? + Rejestracja w serwisie recept w cyfrowej sieci zdrowia jest możliwa dla ubezpieczonych, aptek, praktyk lekarskich i szpitali. + Dlaczego aplikacja e-recepta korzysta z funkcji Google? + Google oferuje funkcje, które można łatwo zintegrować z aplikacjami i które są stale rozwijane i aktualizowane przez Google. Gwarantuje to, że funkcje działają na wielu różnych urządzeniach końcowych i mogą być bezpiecznie obsługiwane. Aplikacja korzysta z funkcji poprawiającej funkcjonalność aparatu i skanowania dla urządzeń z systemem Android (Google ML Kit). + Jak działa ulepszenie skanowania Google ML Kit? + Google ML Kit pomaga zoptymalizować obraz przechwytywany przez aparat, dzięki czemu kody receptur można odczytać nawet w złych warunkach oświetleniowych lub przy użyciu starszych modeli aparatów. + Czy dane dotyczące recepty lub mojego leku zostaną przekazane do Google? + nie Odczytany kod receptury jest zapisywany bezpośrednio w aplikacji. Nie zostaną one przekazane do Google. Dane recepty nie są przechowywane w kodzie, tylko w cyfrowej sieci zdrowia. Stamtąd są one wysyłane do aplikacji. Google nie ma dostępu do cyfrowej sieci zdrowia. + Jakie dane przetwarza Google podczas korzystania z zestawu ML Kit? + Google ma dostęp wyłącznie do informacji technicznych dotyczących używanego urządzenia końcowego i ogólnego korzystania z funkcji dodatkowej (np. współczynnika błędów, ustawień aparatu) w celu rejestrowania ich statystycznie i ulepszania w ten sposób funkcji dodatkowej. Podczas uzyskiwania dostępu Google tymczasowo zapisuje adres IP urządzenia końcowego. Informacje o Tobie i treść przepisu nie będą rejestrowane przez Google. + Czy korzystanie z Google ML Kit jest dobrowolne? + Tak. Jednak ML Kit jest wbudowany w skaner kodów receptur w wersji aplikacji e-recept na Androida. Jeśli używasz skanera kodów receptur na urządzeniu z Androidem, funkcja ML Kit jest również zawsze używana. Możesz jednak obejść się bez korzystania ze skanera kodów receptur. Twoje recepty można również załadować do aplikacji, jeśli zarejestrujesz się w cyfrowej sieci zdrowia za pomocą elektronicznej karty zdrowia lub za pośrednictwem aplikacji ubezpieczenia zdrowotnego. + Czy mogę zobaczyć, kto przeglądał moje przepisy? + Tak. Cały dostęp do twoich danych jest w pełni rejestrowany w cyfrowej sieci zdrowia. W aplikacji e-recepta możesz zobaczyć, kto miał dostęp do Twoich danych. + Z kim mogę się skontaktować, jeśli mam pytania dotyczące aplikacji lub e-recepty? + Szczegółowe informacje można znaleźć w oświadczeniu o ochronie danych. + Przepisana liczba opakowań + Brak recept + W tym celu potrzebujesz recept podlegających zwrotowi. + Wybierz ubezpieczenie + Szukaj ubezpieczenia + Anuluj + O co chcesz wnioskować? + Do tej aplikacji potrzebujesz karty i powiązanego kodu PIN. + Jak chcesz się skontaktować z firmą ubezpieczeniową? + Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu + Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu + Zamknij diff --git a/android/src/main/res/values-ru/strings.xml b/android/src/main/res/values-ru/strings.xml index fc0f0b9d..46ed199d 100644 --- a/android/src/main/res/values-ru/strings.xml +++ b/android/src/main/res/values-ru/strings.xml @@ -6,9 +6,6 @@ Назад в Цифровизация. Оперативность. Надежность. - Добавить рецепты - Вы получили распечатанный рецепт? Для добавления рецептов в приложение отсканируйте код рецепта. - Понятно ID задачи Код доступа Условия использования @@ -32,7 +29,7 @@ Продолжить Давайте начнем Вам потребуется: - Введите номер доступа + Введите номер доступа к карте Ввести PIN-код Попробовать снова Не удалось подключиться к серверу. @@ -60,15 +57,6 @@ Версия: %s Хэш сборки: %s Меню отладки - Код рецепта - Отсканируйте этот код рецепта в аптеке. - Этот сводный код объединяет %s рецепта/-ов - Выкупить в аптеке - Вы находитесь в аптеке и хотите получить препарат по рецепту. - Заказать или зарезервировать - Отправьте свой рецепт в аптеку и решите, как вы хотите получить препараты. - Разрешите определение местоположения и находите аптеки поблизости от вас - Разрешить определение местоположения Открыто до %s Открыто круглосуточно Выходные данные @@ -121,23 +109,14 @@ Вы хотите навсегда удалить этот рецепт? Удалить Отмена - Препараты-заменители допустимы. В соответствии с юридическими требованиями вашей организации медицинского страхования вам может быть выдан альтернативный препарат. - Зарезервировать - Запросить доставку курьером - Заказать отправку по почте - Примите во внимание, что за прописанные препараты может взиматься дополнительная плата. Часы работы Веб-сайт - Выкупить следующие рецепты в %s? Можно выкупить в качестве самостоятельного плательщика только сегодня Войти Активировать NFC Активируйте функцию NFC на своем устройстве, чтобы войти в систему со своей медицинской карточкой. Активировать Исправить - Показать отдельные коды - Показать сводный код - %s из %s Рецепты выкуплены? Отметить рецепты как выкупленные? Не выкуплены @@ -165,7 +144,6 @@ Позвонить на горячую линию Принять участие в опросе +49 800 277 377 7 - Узнать больше Я хочу помочь в работе по улучшению этого приложения Эти данные включают в себя информацию об аппаратном и программном обеспечении вашего телефона, настройках приложения E-Rezept и объеме использования, но никогда не включают информацию о вас или вашем здоровье. Обработчики данных предоставляют информацию только компании gematik GmbH и удаляют ее не позднее чем через 180 дней. Вы можете в любое время деактивировать анализ в меню приложения. @@ -173,12 +151,8 @@ Улучшить приложение Анонимный анализ остается деактивирован %s Спасибо за вашу поддержку! - Указание - При перемещении выкупленных рецептов в архив возможны задержки. - OK Войти Пройдите идентификацию для загрузки рецептов. - Выкуплен %s Информация для аптек: это приложение получает контактные данные и информацию об аптеках с сайта mein-apothekenportal.de Немецкой ассоциации фармацевтов Deutscher Apothekerverband e.V. Вы обнаружили ошибку или хотите исправить данные? Узнать больше Аптеки @@ -189,7 +163,6 @@ Вспомогательные инструменты Изменение масштаба Изменение размеров содержимого в окне приложения сведением или разведением пальцев на экране. - Указание Пароль Защитите свои данные, установив собственный пароль. Пароль @@ -199,8 +172,8 @@ Рекомендации: %s Написать электронное письмо При отправке сообщения будет передана следующая информация об используемом аппаратном обеспечении и операционной системе: - Выкуп будет возможен в ближайшее время - Эта аптека пока не может принимать электронные рецепты. + Выкуп только на сайте + Вы еще не можете отправлять электронные рецепты в эту аптеку. Сейчас открыто Курьерская доставка Отправка @@ -221,10 +194,6 @@ Действует еще %s дней Действует еще %s дней - Открыть сканер - Мы обрабатываем информацию о вашем устройстве!\nДля чтения кодов рецептов данное приложение использует Google ML Kit. Нажимая \"Принять\", вы разрешаете Google время от времени получать доступ к информации о вашем устройстве и обрабатывать ее для анализа использования, диагностики и настройки ML Kit. Вы можете в любое время отозвать свое согласие, что не повлияет на правомерность обработки информации до отзыва согласия. В случае отказа вы не сможете воспользоваться функцией сканирования рецептов. - Соглашаюсь - Отмена Ошибка 20 10 76631 Сертификат вашей медицинской карточки недействителен. Может быть, срок действия вашей карточки истек? Обратитесь в свою организацию медицинского страхования. Безуспешные попытки входа @@ -268,20 +237,11 @@ Введите имя нового профиля. Имя профиля Профили - Добавить профиль - Медицинская карточка - Обратиться в организацию медицинского страхования - Для регистрации в этом приложении вам необходима медицинская карточка, поддерживающая функции NFC, и PIN к ней. - Вы можете бесплатно получить ее в своей организации медицинского страхования. Для этого вам потребуется подтвердить свою личность официальным документом. Как определить, поддерживает ли медицинская карточка функции NFC - Выбрать организацию медицинского страхования - Не выбрано - Что вы хотели бы заказать? Обратиться к ней через это приложение нельзя Свяжитесь со своей страховой организацией по обычным каналам. Медицинская карточка и PIN Только PIN - Обратитесь в свою организацию медицинского страхования Вход в приложение E-Rezept Поле имени не может быть пустым. Профиль с таким именем уже существует. @@ -313,20 +273,17 @@ Порядок изменился с %s: Что происходит, когда вы открываете приложение? Что происходит, когда я использую камеру / считываю рецепты с помощью камеры? - Выбрать профиль - Редактирование профилей Новые рецепты недоступны - рецепт обновлен - рецепта обновлено - рецептов обновлено - рецептов обновлено + новый рецепт + новые рецепты + новые рецепты + новые рецепты Можно выкупить Осуществляется выкуп Выкуплен Неизвестно - Подробности Показать протоколы доступа Здесь вы можете увидеть, кто обращался к вашим рецептам Это ключ для доступа к службе рецептов @@ -363,7 +320,7 @@ Как вы хотели бы продолжить? Заказать Будет доступно в ближайшее время - Зарезервировать сейчас для самовывоза или заказать доставку курьером либо отправку + Забронируйте сейчас для самовывоза или закажите доставку курьерской службой или доставку Сохранить, чтобы заказать позднее Сохранить рецепты на устройстве @@ -375,9 +332,6 @@ Ошибка привязки медицинской карточки Текущий профиль уже привязан к другой медицинской карточке (номер в системе социального страхования %s). Ваша медицинская карточка уже привязана к другому профилю. Перейдите в профиль %s. - Мой заказ - Зарезервировать сейчас - Заказать сейчас Сохранить Контактная информация и адрес Контактная информация @@ -393,23 +347,15 @@ Почтовый индекс и населенный пункт Укажите почтовый индекс и населенный пункт, чтобы с вами можно было связаться. Указания по доставке (необязательно) - Ваш рецепт будет отправлен в указанную аптеку. После этого вы не сможете выкупить его в другой аптеке. - Контактные данные и адрес доставки - Рецепты - Ваши контактные данные необходимы для того, чтобы аптека могла проконсультировать вас и чтобы вы получали информацию о текущем статусе своего заказа. - Ввести контактные данные Необходимы дополнительные контактные данные - Заказ успешно передан - Ваша аптека вскоре свяжется с вами. - Закрыть Отменить изменения? Очистить Для поиска по списку аптек используются геокоординаты, полученные с помощью OpenStreetMap. Мы благодарим этот проект за поддержку. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Использование и защита данных + Конфиденциальность и использование Далее - PIN-код вы получили в письме от организации медицинского страхования. + Вы получили PIN-код в письме от своей страховой компании. PIN-код не получен PIN-код Проверьте соединение с Интернетом и настройки времени/даты на вашем устройстве. @@ -456,23 +402,11 @@ Привязанные устройства Дата регистрации %s (данное устройство) Дата регистрации %s - Актуальные - Архив - Выкупить заново? - Указание: аптека, принимающая рецепт первой, блокирует его обработку другими аптеками. - Отмена - OK - - Вы уже отправили в аптеку %s рецепт. Все равно выкупить заново? - Вы уже отправили в аптеку %s рецепта. Все равно выкупить заново? - Вы уже отправили в аптеку %s рецептов. Все равно выкупить заново? - Вы уже отправили в аптеку некоторые из этих рецептов. Все равно выкупить заново? - В целях безопасности соединение с сервером рецептов прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. PIN-код Введите PIN-код (карточки здоровья) Далее - Аутентификация + Авторизоваться Привязанные устройства Удалить устройство? Отмена @@ -494,8 +428,6 @@ Вам требуется помощь? Мы подобрали ряд советов по решению наиболее часто встречающихся проблем. Показать советы по привязке - Отсканирован: %s - Отсканированный рецепт Разблокировать Карточка заблокирована PIN-код был введен неверно три раза, поэтому ваша карточка заблокирована в целях безопасности. @@ -525,10 +457,9 @@ PIN-код медицинской карточки У вас еще нет медицинской карточки с поддержкой NFC и PIN-кода? Заказать сейчас - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Или: войдите в систему с помощью %s. Приложение вашей организации медицинского страхования - \"Номер доступа (CAN) находится на вашей медицинской карточке в правом верхнем углу\". + \"Номер доступа находится на вашей медицинской карточке в правом верхнем углу\". У моей карточки нет номера доступа У вас осталась еще %s попытка, прежде чем ваша карточка будет заблокирована. @@ -549,6 +480,10 @@ внизу посередине внизу слева Справка + Отправлено %s минут(ы) назад + Отправлено %s + Отправлено только что + Отправлено в %s Недействительно Войти с помощью приложения Выбрать страховую организацию @@ -585,10 +520,9 @@ Фамилия Страховая организация Страховой номер - Номер доступа (CAN) + Номер доступа к карте Войти Выйти - Чтобы получать рецепты автоматически, необходимо войти в систему. Сохранить Изменять Изменить изображение профиля @@ -633,15 +567,13 @@ Аптека Выберите нужный PIN-код Желаемый PIN-код сохранен - Невозможно сохранить желаемый PIN-код E-Rezept В настоящее время открыто и рядом со мной Сортировать по … начать поиск - Отмена - Будет искуплен для вас + Был искуплен за тебя прямое назначение - Выкупить + аптеки Телефонный номер (не обязательно) Поиск по имени или адресу Нет достоверной информации об аптеке @@ -658,10 +590,8 @@ Войти профиль 1 Близко ко мне - У них нет погашаемых рецептов Можно использовать позже Можно получить от %s - %s / %s улучшения продукта Анонимный анализ Помогите нам сделать это приложение лучше. Все пользовательские данные собираются анонимно и используются только для улучшения пользовательского опыта. @@ -701,10 +631,8 @@ нет дозировка Дата выпуска - ночь Этот рецепт будет выкуплен для вас как часть лечения. Нет данных - Нет платы за экстренную помощь дополнительный платеж Препарат Накладные @@ -777,7 +705,117 @@ Пароль не найден На вашей карте не хранится пароль. Вы вышли из системы - Войдите еще раз, чтобы обновить свои рецепты. + Войдите снова, чтобы обновить свои рецепты. номер активного ингредиента мощь и единство + Активирован %s минут назад + Погашено %s + Погашен только что + Погашен в %s часов + заказы + Этот рецепт был выкуплен для вас в рамках лечения. + плата за экстренную помощь + Этот рецепт не может быть выписан ночью в аптеке без дополнительной оплаты сбора за неотложную помощь. + Поищи здесь + Настройки + Поделитесь местоположением в настройках. + Близко ко мне + Удерживайте, чтобы отредактировать имя. + Введите новое имя профиля. + Вы должны войти в систему, чтобы получать цифровые рецепты из вашей практики. + Получать рецепты в цифровом виде? + Перетащите экран вниз, чтобы обновить. + Нет рецептов + Добавляйте рецепты с помощью кнопки + в правом верхнем углу. + Авторизоваться + Погашенные рецепты + Может быть позже + Авторизоваться + Изменить изображение профиля + Погашенные рецепты + Ввести фамилию + Сохранить + Мой заказ + Получатель: в + Рецепты + Аптека + послать + Изменить + Забрать в аптеке + Доставка курьером + Доставка по почте + %s Рецепты + Выкупить невозможно + Не удалось активировать один или несколько рецептов. + Рецепт не выбран + Чтобы активировать рецепты, необходимо выбрать хотя бы один рецепт. + Добавить контактную информацию + Изменить + Без рецепта + В настоящее время у вас нет погашаемых рецептов + поднимать + курьером + Отправка + выбирать рецепты + Нажмите здесь, чтобы отсканировать рецепты + Длительное нажатие для редактирования имен + Добавьте больше профилей, например, для ваших детей или родителей. + Нажмите на дисплей, чтобы пропустить отображаемую всплывающую подсказку. + Как выкупить? + Как бы вы хотели получить лекарство? + Активировать напрямую + Выкупить лекарство на месте + Заказать + Забронируйте или закажите доставку + Готово + коллективный код + отдельные коды + + У вас %s рецепт. + + + У вас есть %s рецептов. + + Сделать выбор + Все рецепты + Какие рецепты? + Далее + Далее + Узнать больше + Указание + Это приложение использует программное обеспечение от Google для распознавания кодов. + Узнать больше + О сканере кода рецепта + Какие данные содержит код рецепта? + Код рецепта содержит только идентификатор рецепта. Это позволяет найти рецепт в службе рецептов в цифровой сети здравоохранения. Код рецепта не содержит никаких данных о вас или вашем лекарстве. + То есть никто ничего не может сделать только с кодом рецепта? + Правильный. Данные рецепта должны быть загружены из службы рецептов. Для этого требуется безопасный вход. + Кто может зарегистрироваться в службе рецептов? + Регистрация в службе рецептов в сети цифрового здравоохранения возможна для застрахованных лиц, аптек, врачебных кабинетов и больниц. + Почему приложение электронных рецептов использует функции Google? + Google предлагает функции, которые можно легко встроить в приложения и которые Google постоянно разрабатывает и обновляет. Это гарантирует, что функции работают на многих различных конечных устройствах и могут работать безопасно. Приложение использует функцию для улучшения функций камеры и сканирования для устройств Android (Google ML Kit). + Как работает улучшение сканирования Google ML Kit? + Google ML Kit помогает оптимизировать изображение, снятое камерой, чтобы коды рецептов можно было считывать даже в условиях плохого освещения или с более старыми моделями камер. + Будут ли данные о рецепте или моем лекарстве передаваться в Google? + нет Прочитанный код рецепта сохраняется прямо в приложении. Он не будет передан в Google. Данные рецепта не хранятся в коде, только в цифровой сети здравоохранения. Оттуда они отправляются в приложение. Google не имеет доступа к сети цифрового здравоохранения. + Какие данные обрабатывает Google при использовании ML Kit? + Google имеет доступ только к технической информации об используемом конечном устройстве и общем использовании дополнительной функции (например, частота ошибок, настройки камеры) только для статистической регистрации и, таким образом, улучшения дополнительной функции. При доступе Google временно записывает IP-адрес вашего конечного устройства. Информация о вас и содержании рецепта не будет записана Google. + Является ли использование Google ML Kit добровольным? + Да. Однако ML Kit встроен в сканер кода рецепта в Android-версии приложения для электронных рецептов. Если вы используете сканер кода рецепта на устройстве Android, функция ML Kit также всегда используется. Однако можно обойтись и без использования сканера кода рецепта. Ваши рецепты также могут быть загружены в приложение, если вы зарегистрируетесь в сети цифрового здравоохранения с помощью электронной карты здоровья или через приложение медицинского страхования. + Могу ли я увидеть, кто просматривал мои рецепты? + Да. Весь доступ к вашим данным полностью регистрируется в цифровой сети здравоохранения. В приложении электронного рецепта вы можете увидеть, кто получил доступ к вашим данным. + С кем я могу связаться, если у меня есть вопросы о приложении или электронном рецепте? + Вы можете найти подробную информацию в заявлении о защите данных. + Предписанное количество упаковок + Нет рецептов + Для этого вам нужны погашаемые рецепты. + Выбрать страховую организацию + Ищите страховку + Отмена + Что вы хотели бы заказать? + Для этого приложения вам нужна карта и соответствующий PIN-код. + Как бы вы хотели связаться со своей страховой компанией? + Ваша страховая компания предлагает следующие варианты контактов + Ваша страховая компания предлагает следующие варианты контактов + Закрыть diff --git a/android/src/main/res/values-tr/strings.xml b/android/src/main/res/values-tr/strings.xml index b6d366ff..a9c7c8e9 100644 --- a/android/src/main/res/values-tr/strings.xml +++ b/android/src/main/res/values-tr/strings.xml @@ -6,9 +6,6 @@ Geri Saat: Dijital. Hızlı. Güvenli. - Reçete ekle - Bir reçete çıktısı mı aldınız? İlgili reçete kodunu tarayarak reçetleri uygulamaya ekleyebilirsiniz. - Anlaşıldı Task-ID Erişim kodu Kullanım şartları @@ -30,7 +27,7 @@ Devam et İşte başlıyoruz İhtiyacınız olan şey: - Erişim numarasını girin + Kart erişim numarasını girin PIN girin Tekrar dene Sunucu bağlantısı başarısız oldu. @@ -56,15 +53,6 @@ Sürüm: %s Build-Hash: %s Hata ayıklama menüsü - Reçete kodu - Bu reçete kodunu eczanenizde tarattırın. - Bu toplu kod %s reçete birleştirir - Eczanede kullan - Bir eczanedesiniz ve reçetenizi kullanmak istiyorsunuz. - Sipariş ver veya rezerve et - Reçetenizi bir eczaneye gönderin ve ilacınızı nasıl almak istediğinize karar verin. - Konumunuzu paylaşın ve bölgenizdeki eczaneleri bulun - Konumu paylaş Şu saate kadar açık: %s Tüm gün açık Künye @@ -117,23 +105,14 @@ Bu reçeteyi kalıcı olarak silmek ister misiniz? Sil İptal et - Muadillere izin verilir. Sağlık sigortanızın yasal gereklilikleri nedeniyle size bir alternatif verilebilir. - Bağlayıcı bir rezerve et - Kurye hizmeti talep edin - Kargo ile teslim ettir - Reçeteli ilaçlar için ek ödemelerin de geçerli olabileceğini lütfen unutmayın. Açılış saatleri Web sitesi - %s\'da bulunan reçeteleri bağlayıcı olarak kullanmak istiyor musunuz? Sacede bugün ve sadece kendiniz ödeyerek kullanabilirsiniz Oturum aç NFC\'yi etkinleştir Sağlık kartınız ile oturum açmak için lütfen cihazınızın NFC fonksiyonunu etkinleştirin. Etkinleştir Düzelt - Tekli kodlar olarak göster - Toplu kod olarak göster - %s / %s Reçeteler kullanıldı mı? Bu reçeteleri kullanılmış olarak işaretlemek ister misiniz? Kullanılmadı @@ -161,7 +140,6 @@ Teknik destek hattını ara Ankete katıl +49 800 277 377 7 - Daha fazla bilgi Bu uygulamayı daha iyi hale getirmek için yardımcı olmak istiyorum Bu, telefonunuzun donanım ve yazılım bilgilerini, e-reçete uygulamasının ayarlarını ve kullanım kapsamını içerir, ancak asla kişiliğiniz veya sağlığınızla ilgili verileri içermez. Veriler, veri işleyenler tarafından sadece gematik GmbH\'ye sunulur ve en geç 180 gün sonra silinir. Analizi istediğiniz zaman uygulama menüsünden devre dışı bırakabilirsiniz. @@ -169,12 +147,8 @@ Uygulamayı iyileştir Anonim analiz devre dışı kalıyor %s Desteğiniz için teşekkürler! - Not - Kullanılan reçetlerin Arşiv alanında görüntülenmesi biraz zaman alabilir. - Tamam Oturum aç Reçeteyi indirmek için lütfen kimliğinizi doğrulayın. - %s tarihinde kullanıldı Eczaneler için not: Eczanelerin iletişim bilgilerini ve bilgilerini Deutscher Apothekenverband e.V.\'ın mein-apothekenportal.de adresinden alıyoruz. Bir hata mı buldunuz veya verileri düzeltmek mi istiyorsunuz? Daha fazla bilgi Ezcaneler @@ -185,7 +159,6 @@ Kullanım yardımı Yakınlaştırma Parmaklarınızı bir araya getirmek veya ayırmak uygulamayı büyütmenizi sağlar (yakınlaştırmak için sıkıştırın). - Not Şifre Verilerinizi kendiniz seçtiğiniz şifre ile koruyun. Şifre @@ -195,8 +168,8 @@ Öneriler: %s E-posta yaz Mesajınızı gönderdiğinizde, kullanılan donanım ve işletim sistemi ile ilgili aşağıdaki bilgiler iletilecektir: - Kullanmak yakında mümkün - Bu ezcane henüz e-reçete kabul edemiyor. + Yalnızca sitede kullan + Henüz bu eczaneye e-reçete gönderemezsiniz. Şu an açık Kurye hizmeti Kargo @@ -213,10 +186,6 @@ %s gün daha geçerli %s gün daha geçerli - Tarayıcıyı aç - Cihaz bilgilerinizi işliyoruz! Bu uygulama, reçete kodunu okumak için Google\'ın ML Kit\'ini kullanır. \"Kabul Et\"i seçerseniz, Google\'ın zaman zaman cihaz bilgilerine erişebileceğini ve bunları ML Kit\'in kullanım analizi, teşhisi ve yapılandırması amacıyla işleyebileceğini kabul edersiniz. Gerçekleşen işlemenin yasallığını etkilemeden onayınızı istediğiniz zaman iptal etme hakkına sahipsiniz. Ancak bu ret, reçete kodu tarayıcısının kullanılamamasına neden olacaktır. - Kabul ediyorum - İptal et Hata 20 10 76631 Sağlık kartınızın sertifikası geçerli değil. Kartınızın süresi dolmuş olabilir mi? Lütfen sağlık sigortanız ile iletişime geçin. Başarısız oturum açma denemesi @@ -258,20 +227,11 @@ Yeni profil için bir ad girin. Profil adı Profiller - Profil ekle - Sağlık kartı - Sağlık sigortanız ile iletişime geçin - Bu uygulamaya giriş yapabilmek için NFC özellikli bir sağlık kartına ve buna ait bir PIN\'e ihtiyacınız var. - Bunu ücretsiz olarak sağlık sigortanızdan temin edebilirsiniz. Bunun için kimliğiniz, resmi kimlik belgeniz ile doğrulanmış olmalıdır. Bu şekilde NFC özellikli sağlık kartını tespit edebilirsiniz - Sağlık sigortanızı seçin - Seçim yok - Neye başvurmak istiyorsunuz? Bu uygulama üzerinden iletişim kurmak mümkün değil. Lütfen sigortanız ile iletişime geçmek için genel geçerli iletişim kanallarını kullanın. Sağlık kartı ve PIN Yalnızca PIN - Sağlık sigortanız ile iletişime geçin E-Rezept uygulamasında oturum açın Ad alanı boş olamaz. Girilen ad ile halihazırda bir profil mevcut. @@ -303,18 +263,15 @@ %s tarihinden bu yana bunlar değişti: Uygulamayı açıtığınızda neler oluyor? Kamera fonksiyonunu kullanırsam/reçeteleri kamera ile tararsam neler oluyor? - Profil seç - Profili düzenle Herhangi bir yeni reçete mevcut değil - %s reçete güncellendi - %s reçete güncellendi + %s yeni̇ reçete + %s yeni reçete Kullanılabilir Reçete aktarıldı Kullanıldı Bilinmiyor - Ayrıntılar Erişim protokollerini göster Burada reçetelerinize kimlerin eriştiğini görebilirsiniz Burada reçete hizmetine olan erişim anahtarı söz konusudur @@ -351,7 +308,7 @@ Nasıl devam etmek istiyorsunuz? Sipariş ver Yakında kullanıma sunulur - Teslim almak için şimdi rezerve edin veya bir kurye hizmeti ya da kargo ile teslim ettirin + Teslim almak için şimdi rezerve edin veya kurye servisi veya nakliye ile teslim ettirin Sonraki siparişler için kaydedin Reçeteleri cihaza kaydedin @@ -361,9 +318,6 @@ Sağlık kartının bağlanması başarısız oldu Güncel profil halihazırda bir başka sağlık kartı (sağlık sigorta numarası %s) ile bağlantılı. Sağlık kartınız halihazırda bir başka profil ile bağlantılı. %s profiline geçin. - Siparişim - Şimdi rezerve et - Şimdi sipariş ver Kaydet İletişim verileri ve adres İletişim @@ -379,23 +333,15 @@ Posta kodu ve yer İletişime geçmek için bir posta kodu ve yeri girin. Teslimat talimatları (opsiyonel) - Bu onay ile reçeteniz bu eczaneye gönderilecektir. Ardından bir başka eczanede kullanamazsınız. - İletişim verileri ve teslimat adresi - Reçeteler - Eczaneden danışma hizmeti almanız ve sizi siparişinizin güncel durumu hakkında bilgilendirmek için iletişim verileriniz gerekli. - İletişim bilgilerini gir Daha fazla iletişim bilgileri gerekli - Sipariş başarıyla aktarıldı - Eczaneniz yakında sizinle iletişime geçecektir. - Kapat Değişiklikler silinsin mi? Sil Ezcane dizini, arama için OpenStreetMap\'in koordinatlarını kullanır. Bu projeye yardımları için teşekkürlerimizi sunarız. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Kullanım ve veri koruma + Gizlilik ve Kullanım İleri - PIN\'iniz posta yoluyla sağlık sigortanızdan aldınız. + PIN\'inizi sağlık sigortanızdan bir mektupla aldınız. PIN alınmadı PIN Lütfen internet bağlantınızı ve cihazınızın saat ile tarih ayarlarını kontrol edin. @@ -442,21 +388,11 @@ Bağlı cihazlar %s tarihinden beri kayıtlı (bu cihaz) %s tarihinden beri kayıtlı - Güncel - Arşiv - Tekrar kullanılsın mı? - Uyarı: Reçeteni ilk olarak kabul eden ezcane diğer ezcanaler için reçeteyi bloke eder. - İptal et - Tamam - - %s reçetesini zaten bir ezcaneye yolladınız. Yine de yeniden kullanmak istiyor musunuz? - Bu reçetelerin bazılarını zaten bir ezcaneye yolladınız. Yine de başka eczaneye yollamak istiyor musunuz? - Güvenlik nedeniyle, reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. PIN PIN\'inizi (sağlık kartı) girin. İleri - Kimlik doğrulama + Giriş yapmak Bağlı cihazlar Cihazı çıkarmak istiyor musunuz? İptal et @@ -478,8 +414,6 @@ Yardım mı istiyorsunuz? En sık karşılaştığınız sorunları çözmek için sizin için birkaç ip uçu derledik. Bağlantı ip uçlarını başlat - Şu tarihte tarandı: %s - Taranmış reçete Blokeyi kaldır Kart bloke edildi PIN üç kez yanlış girildi. Güvenlik nedenlerinden dolayı kartınız bloke edildi. @@ -509,10 +443,9 @@ Sağlık kartının PIN\'i NFC özellikli sağlık kartına ve PIN\'e henüz sahip değil misiniz? Şimdi sipariş verin - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Veya: %s ile oturum açın. Sağlık sigortanızın uygulaması - "Erişim numaranızı (Card Access Number, kısaca: CAN) sağlık kartınızın sağ üst köşesinde bulacaksınız." + "Erişim numaranızı sağlık kartınızın sağ üst köşesinde bulacaksınız." Kartımın erişim numarası yok Kartınız bloke edilmeden önce %s denemeniz var. @@ -531,6 +464,10 @@ Orta alt alanda Sol alt alanda Yardım + %s dakika önce gönderildi + Şu saatte gönderildi: %s + Şu anda gönderildi + Şu saatte gönderildi: %s Artık geçerli değil Uygulama ile oturum aç Sigortayı seç @@ -567,10 +504,9 @@ Ad sigorta Sigorta numarası - Erişim numarası (CAN) + Kart erişim numarası Oturum aç Oturumu kapat - Reçeteleri otomatik olarak almak için oturum açmalısınız. Kaydet Değiştirmek Profil resmini düzenle @@ -615,15 +551,13 @@ eczane İstediğiniz PIN\'i seçin İstenen PIN kaydedildi - İstenen PIN\'i kaydetmek mümkün değil E-Rezept Şu anda açık ve yakınımda Tarafından filtre … Aramaya başla - İptal et - Senin için kurtarılacak + Senin için kurtarıldı doğrudan atama - Kullan + eczaneler telefon numarası (isteğe bağlı) Ada veya adrese göre arama Geçerli eczane bilgisi yok @@ -640,10 +574,8 @@ Oturum aç profil 1 Bana yakın - Kullanılabilir reçeteleri yok Daha sonra kullanılabilir %s kullanılabilir - %s / %s ürün iyileştirmeleri Anonim Analiz Bu uygulamayı daha iyi hale getirmemize yardımcı olun. Tüm kullanıcı verileri anonim olarak toplanır ve yalnızca kullanıcı deneyimini geliştirmek için kullanılır. @@ -681,10 +613,8 @@ hayır dozaj Veriliş tarihi - gece - Bu reçete sizin için bir tedavinin parçası olarak kullanılacaktır. + Bu reçete, tedavinin bir parçası olarak sizin için kullanılacaktır. Bilgi yok - Acil servis ücreti yok Ek ödeme uyuşturucu Teslimat notları @@ -757,7 +687,115 @@ Şifre bulunamadı Kartınızda kayıtlı şifre yok. Çıkış yaptınız - Tariflerinizi güncellemek için tekrar oturum açın. + Tariflerinizi güncellemek için tekrar giriş yapın. aktif madde numarası güç ve birlik + %s dakika önce kullanıldı + %s tarihinde kullanıldı + Az önce kullanıldı + Saat %s konumunda kullanıldı + emirler + Bu reçete, bir tedavinin parçası olarak sizin için kullanıldı. + acil servis ücreti + Bu reçete, acil servis ücreti ek ödemesi yapılmadan gece eczanede doldurulamaz. + Burada ara + Ayarlar + Ayarlarda konumu paylaşın. + Bana yakın + Adı düzenlemek için basılı tutun. + Profil için yeni adı girin. + Muayenehanenizden dijital reçeteler almak için giriş yapmalısınız. + Yemek tarifleri dijital olarak alınsın mı? + Yenilemek için ekranı aşağı sürükleyin. + Reçete yok + Sağ üst köşedeki + düğmesini kullanarak tarifler ekleyin. + Giriş yapmak + Kullanılan reçeteler + Belki sonra + Giriş yapmak + Profil resmini düzenle + Kullanılmış reçeteler + İsim giriniz + Kaydet + Siparişim + alıcı: içinde + Reçeteler + eczane + Göndermek + Değişmek + eczaneden al + kurye ile teslimat + Posta ile teslimat + %s Tarifler + Kullanılamaz + Bir veya daha fazla reçete kullanılamadı. + Tarif seçilmedi + Tarifleri kullanmak için en az bir tarif seçilmelidir. + İletişim bilgilerini ekleyin + Değişmek + reçete yok + Şu anda kullanılabilecek reçeteniz yok + toplamak + kurye + Kargo + yemek tarifleri seç + Tarifleri taramak için buraya dokunun + Adları düzenlemek için uzun basın + Örneğin çocuklarınız veya ebeveynleriniz için daha fazla profil ekleyin. + Görüntülenen araç ipucunu atlamak için ekrana tıklayın. + Nasıl geri alınır? + İlaçlarınızı nasıl almak istersiniz? + Doğrudan kullan + Sitede ilaç kullanın + Sipariş ver + Rezerve edin veya teslim ettirin + Bitti + kolektif kod + tek kodlar + + %s reçeteniz var. + %s tarifiniz var. + + seçim yap + Tüm tarifler + Hangi tarifler? + İleri + İleri + Daha fazla bilgi + Not + Bu uygulama, kodları tanımak için Google\'ın yazılımını kullanır. + Daha fazla bilgi + Tarif kodu tarayıcı hakkında + Reçete kodu hangi verileri içerir? + Reçete kodu sadece tarifin tanımlayıcısını içerir. Bu, reçetenin dijital sağlık ağındaki reçete hizmetinde bulunmasını sağlar. Reçete kodu, sizinle veya ilacınızla ilgili herhangi bir veri içermez. + Yani kimse sadece tarif koduyla bir şey yapamaz mı? + Doğru. Reçete verilerinin reçete servisinden indirilmesi gerekir. Bu, güvenli bir oturum açma gerektirir. + Reçete hizmetine kimler kayıt olabilir? + Sigortalıların, eczanelerin, muayenehanelerin ve hastanelerin dijital sağlık ağında reçete hizmetine kaydolması mümkündür. + E-reçete uygulaması neden Google özelliklerini kullanıyor? + Google, uygulamalara kolayca yerleştirilebilen ve Google tarafından sürekli olarak geliştirilip güncellenen işlevler sunar. Bu, fonksiyonların birçok farklı uç cihazda çalışmasını ve güvenli bir şekilde çalıştırılabilmesini sağlar. Uygulama, Android cihazlar (Google ML Kit) için kamera ve tarama işlevini geliştirmeye yönelik bir özellik kullanır. + Google ML Kit tarama iyileştirmesi nasıl çalışır? + Google ML Kit, bir kamera tarafından çekilen görüntüyü optimize etmeye yardımcı olur, böylece tarif kodları zayıf aydınlatma koşullarında veya daha eski kamera modellerinde bile okunabilir. + Reçete veya ilacımla ilgili veriler Google\'a aktarılacak mı? + hayır Okunan tarif kodu doğrudan uygulamaya kaydedilir. Google\'a aktarılmayacaktır. Reçete verileri kodda değil, yalnızca dijital sağlık ağında saklanır. Oradan uygulamaya gönderilirler. Google\'ın dijital sağlık ağına erişimi yoktur. + Google, ML Kit kullanırken hangi verileri işler? + Google, bunu istatistiksel olarak kaydetmek ve böylece ek işlevi iyileştirmek için yalnızca kullanılan son cihaz ve ek işlevin genel kullanımı (örn. hata oranı, kamera ayarları) hakkındaki teknik bilgilere erişebilir. Eriştiğinizde Google, uç cihazınızın IP adresini geçici olarak kaydeder. Hakkınızdaki bilgiler ve tarifin içeriği Google tarafından kaydedilmeyecektir. + Google ML Kit\'in kullanımı gönüllü mü? + Evet. Ancak ML Kit, e-reçete uygulamasının Android sürümündeki reçete kodu tarayıcısına yerleştirilmiştir. Tarif kodu tarayıcıyı bir Android cihazda kullanıyorsanız, ML Kit işlevi de her zaman kullanılır. Ancak, tarif kodu tarayıcısını kullanmadan yapabilirsiniz. Elektronik sağlık kartı veya sağlık sigortası uygulamanız üzerinden dijital sağlık ağına kaydolursanız reçeteleriniz de uygulamaya yüklenebilir. + Tariflerime kimlerin baktığını görebilir miyim? + Evet. Verilerinize tüm erişimler tamamen dijital sağlık ağında kaydedilir. E-reçete uygulamasında verilerinize kimlerin ulaştığını görebilirsiniz. + Uygulama veya e-reçete ile ilgili sorularım olursa kiminle iletişime geçebilirim? + Ayrıntılı bilgiyi veri koruma beyanında bulabilirsiniz. + Reçete edilen paket sayısı + Reçete yok + Bunun için paraya çevrilebilir reçetelere ihtiyacınız var. + Sigortayı seç + sigorta ara + İptal et + Neye başvurmak istiyorsunuz? + Bu uygulama için bir karta ve ilgili PIN\'e ihtiyacınız var. + Sigorta şirketinizle nasıl iletişime geçmek istersiniz? + Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar + Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar + Kapat diff --git a/android/src/main/res/values-uk/strings.xml b/android/src/main/res/values-uk/strings.xml index 48d773b1..dd21c4c0 100644 --- a/android/src/main/res/values-uk/strings.xml +++ b/android/src/main/res/values-uk/strings.xml @@ -6,9 +6,6 @@ Назад о Цифрово. Швидко. Надійно. - Додати рецепти - Ви отримали видрук рецепта? Додайте рецепти до застосунку, відсканувавши відповідний код рецепта. - Зрозуміло Ідентифікатор завдання Код доступу Умови використання @@ -20,7 +17,7 @@ Це недійсний код рецепта Цей код рецепта уже відскановано - Розпізнано один рецепт + Розпізнано %s рецепт Розпізнано %s рецепти Розпізнано %s рецептів @@ -32,13 +29,13 @@ Продовжити Почнімо Вам потрібно: - Введіть номер доступу + Введіть номер доступу до картки Ввести PIN-код Спробуйте ще раз Помилка з’єднання з сервером Введено неправильний PIN-код. - У вас ще одна спроба, перш ніж буде заблоковано картку + У вас ще %s спроба, перш ніж буде заблоковано картку У вас ще %s спроби, перш ніж буде заблоковано картку У вас ще %s спроб, перш ніж буде заблоковано картку @@ -60,15 +57,6 @@ Версія: %s Хеш збірки: %s Меню налагодження - Код рецепта - Відскануйте цей код рецепта у своїй аптеці. - Цей збірний код об’єднує %s рецептів - Погасити в аптеці - Ви стоїте в аптеці й хочете отримати лікарства по рецепту. - Замовити або зарезервувати - Відправте свій рецепт в аптеку і вкажіть, як ви хочете отримати ліки. - Поділіться місцем розташування та знайдіть аптеки у вашому регіоні - Поділитися місцем розташування Відчинено до %s Відчинено без перерв Вихідні дані @@ -121,23 +109,14 @@ Видалити цей рецепт безповоротно? Видалити Скасувати - Замінники дозволені. Відповідно до законодавчих вимог вашої лікарняної каси вам можуть надати альтернативний медикамент. - Зарезервувати з зобов’язанням придбання - Зробити запит кур\'єрської служби - Доставити поштою - Зауважте, що за виписані ліки також може нараховуватися доплата. Графік роботи: Вебсайт - Бажаєте викупити наступні рецепти в %s, що матиме обов’язковий характер? Ще тільки сьогодні рецепт можна погасити, якщо ви оплачуєте самостійно. Вхід Активувати NFC Активуйте функцію NFC на своєму пристрої, щоб увійти за допомогою своєї картки здоров’я. Активувати Відкоректувати - Показати як окремі коди - Показати як збірний код - %s з %s Рецепти погашено? Позначити рецепти як погашені? Не погашено @@ -165,7 +144,6 @@ Зателефонувати на технічну гарячу лінію Взяти участь в опитуванні +49 800 277 377 7 - Детальніше Я хочу допомогти покращити цей застосунок Це включає інформацію про апаратне та програмне забезпечення на вашому телефоні, налаштування застосунку E-Rezept та обсяг використання, однак у жодному разі не дані про вас особисто чи ваше здоров’я. Ці дані надаються тільки gematik GmbH компанією, яка обробляє дані, та видаляються не пізніше ніж через 180 днів. Аналіз можна деактивувати в меню застосунку в будь-який час. @@ -173,12 +151,8 @@ Покращити застосунок Анонімний аналіз залишається деактивованим %s Дякуємо за Вашу підтримку! - Указівка - Перед тим, як погашені рецепти будуть переміщені в архів, може виникнути затримка. - Ok Вхід Щоб завантажити рецепти, ідентифікуйте себе. - Дата погашення: %s Примітка для аптек: ми отримуємо контактні дані та інформацію про аптеки від mein-apothekenportal.de Німецької аптечної асоціації Deutsche Apothekenverband e.V. Ви виявили помилку чи хотіли б виправити дані? Детальніше Аптеки @@ -189,7 +163,6 @@ Довідки з керування Масштабувати Дозволяє збільшувати масштабування застосунку зведенням або розведенням пальців (зведення для масштабування). - Указівка Пароль: Захистіть свої дані паролем на свій вибір. Пароль @@ -199,8 +172,8 @@ Рекомендації: %s Написати електронне повідомлення Під час надсилання своїх повідомлень буде передаватися вказана нижче інформація про обладнання та операційну систему, що використовується: - Погасити якомога швидше - Наразі ця аптека ще не може приймати електронні рецепти. + Викуповувати лише на сайті + Ви ще не можете надсилати електронні рецепти в цю аптеку. Наразі відчинено Кур\'єрська служба Розсилання @@ -210,26 +183,22 @@ Зрозуміло Паролі не збігаються - Залишається ще один день, щоб погасити рецепт, якщо ви оплачуєте самостійно. + Залишається ще %s день, щоб погасити рецепт, якщо ви оплачуєте самостійно. Залишається ще %s дні, щоб погасити рецепт, якщо ви оплачуєте самостійно. Залишається ще %s днів, щоб погасити рецепт, якщо ви оплачуєте самостійно. - Діє ще один день + Діє ще %s день Діє ще %s дні Діє ще %s днів - Відкрити сканер - Ми обробляємо інформацію про ваш пристрій!\nЦей застосунок використовує набір ML Kit від Google для читання коду рецепта. Якщо ви виберете «Прийняти», ви погоджуєтеся з тим, що Google може час від часу отримувати доступ до інформації пристрою та обробляти її з метою аналізу використання, діагностики та конфігурації ML Kit. Ви маєте право в будь-який час відкликати свою згоду, не впливаючи на законність виконаної обробки. Однак відхилення призведе до того, що ви не зможете використовувати сканер коду рецепта. - Погоджуюся - Скасувати Помилка 20 10 76631 Сертифікат вашої картка здоров\'я недійсний. Можливо, термін дії вашої картки закінчився? Зв’яжіться зі своєю медичною страховою компанією. Невдалі спроби входу - Виявлено одну вдалу спробу входу в систему. + Виявлено %s вдалу спробу входу в систему. Виявлено %s вдалі спроби входу в систему. Виявлено %s вдалих спроб входу в систему. @@ -268,20 +237,11 @@ Введіть ім\'я для нового профілю. Ім\'я профілю Профілі - Додати профіль - Картка здоров\'я - Зверніться до медичної страхової компанії - Щоб увійти в цей застосунок, вам потрібна картка здоров\'я з підтримкою NFC та відповідний PIN-код. - Її ви отримаєте безкоштовно від своєї медичної страхової компанії. Для цього ви повинні ідентифікувати себе за допомогою офіційного документа, що посвідчує особу. Так можна розпізнати картку здоров\'я з підтримкою NFC - Вибрати медичну страхову компанію - Вибір відсутній - Бажаєте подати заявку? Зв\'язатися через цей застосунок не можливо Використовуйте звичайні канали, щоб зв\'язатися зі своєю страховою компанією. Картка здоров’я та PIN Тільки PIN - Зв\'яжіться зі своєю медичною страховою компанією Вхід у застосунок E-Rezept Поле прізвища не може бути порожнім. Профіль із введеним іменем уже існує. @@ -313,20 +273,17 @@ З %s відбулися зміни: Що буде, коли ви відкриєте застосунок? Що станеться, якщо я використаю функцію камери / зчитаю рецепти за допомогою камери? - Вибрати профіль - Редагувати профілі Нових рецептів немає - Один рецепт актуалізовано - %s рецепти актуалізовано - %s рецептів актуалізовано - + %s новий рецепт + + + %s нових рецепта Можна погасити Погашається Погашено Невідомо - Деталі Відобразити протоколи доступу Тут ви можете побачити, хто мав доступ до ваших рецептів Мова про ключ доступу до сервісу рецептів @@ -363,11 +320,11 @@ Бажаєте продовжити? Замовити Незабаром буде в наявності - Забронюйте зараз для самовивозу, доставлення кур’єрською службою або поштою + Забронюйте зараз для отримання або доставте його кур\'єрською службою чи доставкою Зберегти для пізніших замовлень Зберегти рецепти на пристрої - Далі з одним рецептом + Далі з %s рецептом Далі з %s рецептами Далі з %s рецептами @@ -375,9 +332,6 @@ Помилка підключення картки здоров’я Поточний профіль уже підключений до іншої картки здоров\'я (номер медичного страхування: %s). Ваша картка здоров\'я вже прив\'язана до іншого профілю. Перейдіть до профілю %s. - Моє замовлення - Зарезервувати зараз - Замовити зараз Зберегти Контактні дані й адреса Контакт @@ -393,23 +347,15 @@ Індекс і нас. пункт Введіть поштовий індекс і населений пункт для контакту. Інструкція з доставлення (опція) - Таким чином, ваш рецепт буде надіслано в цю аптеку. Після цього ви не зможете погасити його в жодній іншій аптеці. - Контактні дані й адреса доставлення - Рецепти - Нам потрібні ваші контактні дані для консультації в аптеці та для інформування вас про поточний стан вашого замовлення. - Надати контактні дані Потрібні подальші контактні дані - Замовлення успішно переслано - Незабаром ваша аптека зв’яжеться з вами. - Закрити Скинути зміни? Скинути Для пошуку в довіднику аптек використовуються геолокацію, визначені за допомогою OpenStreetMap. Ми дякуємо проєкту за цю допомогу. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Користування і захист даних + Конфіденційність і використання Далі - Ви отримали свій PIN-код у листі від вашої медичної страхової компанії. + Ви отримали PIN-код у листі від вашої страхової компанії. PIN-код не отримано PIN Перевірте підключення до Інтернету та налаштування часу/дати на вашому пристрої. @@ -456,23 +402,11 @@ Підключені пристрої Зареєстровано з %s (цей пристрій) Зареєстровано з %s - Актуальний - Архів - Погасити повторно? - Примітка. Аптека, яка першою приймає рецепт, блокує його обробку в іншій аптеці. - Скасувати - Ok - - Ви уже відправили рецепт %s в одну аптеку. Погасити повторно? - Ви уже відправили декілька рецептів в одну аптеку. Погасити повторно? - Ви уже відправили декілька рецептів в одну аптеку. Погасити повторно? - Ви уже відправили декілька рецептів в одну аптеку. Погасити повторно? - З міркувань безпеки підключення до сервера рецептів припиняється через 12 годин. Для повторного підключення вам знадобиться медична картка та PIN-код для кожного процесу підключення. PIN Ввести PIN-код (картки здоров’я) Далі - Аутентифікація + Увійти Підключені пристрої Видалити пристрій? Скасувати @@ -494,8 +428,6 @@ Вам потрібна допомога? Ми зібрали для вас поради щодо розв\'язання найпоширеніших проблем. Запустити поради щодо підключення - Дата сканування: %s - Відсканований рецепт Розблокувати Картка заблокована PIN-код тричі введено неправильно. Таким чином, ваша картка заблокована з міркувань безпеки. @@ -525,13 +457,12 @@ PIN-код картки здоров’я У вас ще немає картки здоров’я з підтримкою NFC та PIN-коду до неї? Замовити зараз - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Або: увійдіть за допомогою %s. Застосунок вашої медичної страхової компанії - "Ваш номер доступу (Card Access Number, скорочено CAN) ви знайдете у верхньому правому куті своєї картки здоров\'я." + "Ваш номер доступу ви знайдете у верхньому правому куті своєї картки здоров\'я." Моя картка не має номера доступу - У вас ще одна спроба, перш ніж буде заблоковано картку + У вас ще %s спроба, перш ніж буде заблоковано картку У вас ще %s спроби, перш ніж буде заблоковано картку У вас ще %s спроб, перш ніж буде заблоковано картку @@ -549,6 +480,10 @@ у нижній частині по центру у нижній частині зліва Довідка + Відправлено %s хв тому + Дата відправлення: %s + Відправлено щойно + Час відправлення: %s Більше не дійсний Увійдіть за допомогою застосунку Вибрати страховку @@ -585,10 +520,9 @@ Прізвище Страхування Страховий номер - Номер доступу (CAN) + Номер доступу до картки Вхід Вийти з системи - Щоб автоматично отримувати рецепти, ви повинні увійти в систему. Зберегти Зміна Редагувати зображення профілю @@ -633,15 +567,13 @@ Аптека Виберіть потрібний PIN-код Бажаний PIN-код збережено - Неможливо зберегти потрібний PIN-код E-Rezept Наразі відкрито і біля мене Фільтрувати за… почати пошук - Скасувати - Буде викуплено для вас + Був викуплений для вас пряме доручення - Погасити + аптеках номер телефону (необов\'язково) Пошук за прізвищем або адресою Немає дійсної інформації про аптеку @@ -658,10 +590,8 @@ Вхід профіль 1 Поруч зі мною - У них немає рецептів, які можна викупити Викупити пізніше Можна отримати від %s - %s / %s вдосконалення продукту Анонімний аналіз Допоможіть нам зробити цю програму кращою. Усі дані користувача збираються анонімно та використовуються лише для покращення взаємодії з користувачем. @@ -701,10 +631,8 @@ ні дозування дата випуску - noctu Цей рецепт буде використано для вас як частина лікування. Дані відсутні - Немає плати за екстрену службу додаткова оплата Медикамент Накладні на доставку @@ -780,4 +708,114 @@ Увійдіть знову, щоб оновити свої рецепти. номер активного інгредієнта потужність і єдність + Використано %s хвилин тому + Використано %s + Викупив щойно + Використано о %s годині + замовлення + Цей рецепт був викуплений для вас як частина лікування. + плата за екстрену службу + Цей рецепт не можна отримати вночі в аптеці без додаткової оплати екстреної послуги. + Шукайте тут + Налаштування + Поділитися місцезнаходженням у налаштуваннях. + Поруч зі мною + Утримуйте, щоб редагувати назву. + Введіть нову назву для профілю. + Ви повинні увійти в систему, щоб отримати цифрові рецепти від вашої практики. + Отримувати рецепти в цифровому вигляді? + Перетягніть екран вниз, щоб оновити. + Кожних рецептів + Додайте рецепти за допомогою кнопки + у верхньому правому куті. + Увійти + Викуплені рецепти + Можливо пізніше + Увійти + Редагувати зображення профілю + Викуплені рецепти + Введіть ім\'я + Зберегти + Моє замовлення + Адресат: в + Рецепти + Аптека + Надіслати + Змінювати + Забрати в аптеці + Доставка кур\'єром + Доставка поштою + %s Рецепти + Викупити неможливо + Один або кілька рецептів не вдалося викупити. + Рецепт не вибрано + Щоб отримати рецепти, потрібно вибрати принаймні один рецепт. + Додайте контактну інформацію + Змінювати + Без рецепта + Наразі у вас немає рецептів, які можна викупити + пікап + кур\'єр + Розсилання + вибрати рецепти + Торкніться тут, щоб сканувати рецепти + Тривале натискання для редагування імен + Додайте більше профілів, наприклад, для ваших дітей або батьків. + Клацніть на дисплеї, щоб пропустити відображену підказку. + Як викупити? + Як би ви хотіли отримати ваші ліки? + Викупити безпосередньо + Викупити ліки на місці + Замовити + Забронюйте або доставте + Готово + колективний код + одиничні коди + + Ви маєте %s рецепт. + + + У вас є %s рецептів. + + Вибрати + Всі рецепти + Які рецепти? + Далі + Далі + Детальніше + Указівка + Ця програма використовує програмне забезпечення від Google для розпізнавання кодів. + Детальніше + Про сканер кодів рецептів + Які дані містить код рецепта? + Код рецепту містить лише ідентифікатор рецепту. Це дозволяє знайти рецепт у службі рецептів у цифровій мережі охорони здоров’я. Код рецепта не містить жодних даних про вас чи ваші ліки. + Тож ніхто нічого не може зробити лише з кодом рецепту? + Правильно. Дані про рецепт необхідно завантажити зі служби рецептів. Для цього потрібен безпечний вхід. + Хто може зареєструватися в рецептурній службі? + Реєстрація в службі рецептів у цифровій мережі охорони здоров’я можлива для застрахованих осіб, аптек, медичних практик і лікарень. + Чому додаток для електронних рецептів використовує функції Google? + Google пропонує функції, які можна легко вбудувати в програми та які Google постійно розробляє та оновлює. Це гарантує, що функції працюють на багатьох різних кінцевих пристроях і ними можна безпечно керувати. Додаток використовує функцію для покращення камери та функцій сканування для пристроїв Android (Google ML Kit). + Як працює покращення сканування Google ML Kit? + Google ML Kit допомагає оптимізувати зображення, зняте камерою, щоб коди рецептів можна було прочитати навіть в умовах поганого освітлення або за допомогою старих моделей камер. + Чи будуть дані про рецепт або мої ліки передані в Google? + немає Прочитаний код рецепта зберігається безпосередньо в додатку. Його не буде передано Google. Дані про рецепти не зберігаються в коді, лише в цифровій мережі охорони здоров’я. Звідти вони надсилаються в додаток. Google не має доступу до цифрової мережі охорони здоров’я. + Які дані обробляє Google під час використання ML Kit? + Google має доступ лише до технічної інформації про використовуваний кінцевий пристрій і загальне використання додаткової функції (наприклад, частота помилок, налаштування камери), щоб статистично зафіксувати це й таким чином покращити додаткову функцію. Під час доступу Google тимчасово записує IP-адресу вашого кінцевого пристрою. Інформація про вас і вміст рецепта не будуть записані Google. + Чи є використання Google ML Kit добровільним? + Так. Однак ML Kit вбудовано в сканер кодів рецептів у версії програми для електронних рецептів для Android. Якщо ви використовуєте сканер кодів рецептів на пристрої Android, функція ML Kit також використовується завжди. Однак можна обійтися і без використання сканера кодів рецептів. Ваші рецепти також можна завантажити в додаток, якщо ви зареєструєтесь у цифровій мережі охорони здоров’я за допомогою електронної медичної картки або через програму медичного страхування. + Чи можу я побачити, хто переглядав мої рецепти? + Так. Весь доступ до ваших даних повністю реєструється в цифровій мережі охорони здоров’я. У додатку для електронних рецептів ви можете побачити, хто мав доступ до ваших даних. + До кого я можу звернутися, якщо у мене є запитання щодо програми чи електронного рецепта? + Ви можете знайти детальну інформацію в декларації про захист даних. + Приписана кількість упаковок + Кожних рецептів + Для цього вам потрібні викупні рецепти. + Вибрати страховку + Шукайте страховку + Скасувати + Бажаєте подати заявку? + Для цієї програми вам потрібна картка та відповідний PIN-код. + Як би ви хотіли зв’язатися зі своєю страховою компанією? + Ваша страхова компанія пропонує такі способи зв’язку + Ваша страхова компанія пропонує такі способи зв’язку + Закрити diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index a3e29825..dad99aed 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -6,9 +6,6 @@ Zurück um Digital. Schnell. Sicher. - Rezepte hinzufügen - Sie haben einen Rezept-Ausdruck erhalten? Rezepte fügen Sie der App hinzu, indem Sie den jeweiligen Rezeptcode abscannen. - Verstanden Task-ID Access-Code Nutzungsbedingungen @@ -30,7 +27,7 @@ Nicht abbrechen Los geht’s Was Sie benötigen: - Zugangsnummer eingeben + Kartenzugangsnummer eingeben PIN eingeben Erneut probieren Verbindung mit dem Server herstellen fehlgeschlagen. @@ -60,15 +57,6 @@ Version: %s Build-Hash: %s Debug-Menü - Rezeptcode - Lassen Sie diesen Rezeptcode in Ihrer Apotheke abscannen. - Dieser Sammelcode bündelt %s Rezepte - In Apotheke einlösen - Sie stehen in einer Apotheke und möchten Ihr Rezept einlösen. - Bestellen oder reservieren - Senden Sie Ihr Rezept an eine Apotheke und entscheiden Sie, wie Sie Ihre Medikamente erhalten möchten. - Standort freigeben und Apotheken in Ihrer Umgebung finden - Standort freigeben Geöffnet bis %s Uhr Durchgehend geöffnet Impressum @@ -123,23 +111,14 @@ Möchten Sie dieses Rezept unwiderruflich löschen? Löschen Abbrechen - Ersatzpräparate sind zulässig. Aufgrund gesetzlicher Vorgaben Ihrer Krankenversicherung kann Ihnen eine Alternative ausgehändigt werden. - Verbindlich reservieren - Botendienst anfragen - Per Versand liefern lassen - Bitte beachten Sie, dass auch für verschriebene Medikamente Zuzahlungen anfallen können. Öffnungszeiten Webseite - Möchten Sie folgende Rezepte in der %s verbindlich einlösen? Nur noch heute als Selbstzahlender einlösbar Anmelden NFC aktivieren Bitte aktivieren Sie die NFC-Funktion Ihres Geräts, um sich mit Ihrer Gesundheitskarte anzumelden. Aktivieren Korrigieren - Als Einzelcodes anzeigen - Als Sammelcode anzeigen - %s von %s Rezepte eingelöst? Möchten Sie die Rezepte als eingelöst markieren? Nicht eingelöst @@ -154,7 +133,7 @@ Okay Screenshots unterdrücken Verhindert die Anzeige eines Vorschaubilds beim App-Wechsel - Erlauben Sie E-Rezept Ihr Nutzerverhalten anonym zu analysieren? + Erlauben Sie E-Rezept Ihr Nutzungsverhalten anonym zu analysieren? Technische Informationen Sicherheit Ihrer Rezeptdaten Bitte achten Sie darauf, dass Personen, mit denen Sie gegebenenfalls dieses Gerät teilen und deren biometrische Merkmale auf diesem Gerät gespeichert sein könnten, ebenfalls Zugriff auf Ihre Rezepte erhalten. @@ -168,21 +147,16 @@ An Umfrage teilnehmen +49 800 277 377 7 app-feedback@gematik.de - Mehr erfahren Ich möchte dabei helfen, diese App besser zu machen Das umfasst Hard- und Softwareinformationen Ihres Telefons, Einstellungen der E-Rezept App sowie Umfang der Nutzung, jedoch niemals Daten über Ihre Person oder Ihre Gesundheit. Die Daten werden durch Datenverarbeitungsnehmer nur der gematik GmbH zur Verfügung gestellt und spätestens nach 180 Tagen gelöscht. Sie können die Analyse jederzeit wieder im Menü der App deaktivieren. - Wir können durch diese Daten nachvollziehen, welche Funktionen häufig genutzt werden, und diese verbessern. Ferner können wir einschätzen, wie lange ältere Technik unterstützt werden muss, und wann wir z.B. eine neuere Betriebssystemversion verpflichtend machen können, ohne (zu viele) Nutzer zu betreffen. + Wir können durch diese Daten nachvollziehen, welche Funktionen häufig genutzt werden, und diese verbessern. Ferner können wir einschätzen, wie lange ältere Technik unterstützt werden muss, und wann wir z.B. eine neuere Betriebssystemversion verpflichtend machen können, ohne (zu viele) Nutzende zu betreffen. App verbessern Anonyme Analyse bleibt deaktiviert %s Vielen Dank für Ihre Unterstützung! \u2661 - Hinweis - Es kann zu einer Verzögerung kommen, bis eingelöste Rezepte in das Archiv verschoben werden. - Okay Anmelden Bitte identifizieren Sie sich um Rezepte herunterzuladen. - Eingelöst am %s Hinweis für die Apotheken: Die Kontaktdaten und Informationen zu Apotheken beziehen wir von mein-apothekenportal.de des Deutschen Apothekenverbandes e.V. Sie haben einen Fehler entdeckt oder möchten Daten korrigieren? mein-apothekenportal.de Mehr erfahren @@ -196,7 +170,6 @@ Bedienungshilfen Zoomen Ermöglicht das Vergrößern der App über das Zusammen- oder Auseinanderziehen der Finger (Pinch-to-Zoom). - Hinweis Kennwort Sichern Sie Ihre Daten mit einem selbstgewählten Passwort. Kennwort @@ -206,8 +179,8 @@ Empfehlungen: %s Mail schreiben Beim Senden Ihrer Nachricht werden folgende Informationen über genutzte Hardware und Betriebssystem übertragen: - Einlösen bald möglich - Diese Apotheke kann derzeit noch keine E-Rezepte in Empfang nehmen. + Einlösen nur vor Ort + An diese Apotheke können Sie noch keine E-Rezepte senden. Aktuell geöffnet Botendienst Versand @@ -224,10 +197,6 @@ Noch %s Tag gültig Noch %s Tage gültig - Scanner öffnen - Wir verarbeiten Ihre Geräteinformationen!\nZum Lesen des Rezeptcodes nutzt diese App das ML Kit von Google. Wenn Sie „Akzeptieren“ auswählen, stimmen Sie zu, dass Google von Zeit zu Zeit auf Geräteinformationen zugreifen und diese zum Zwecke der Nutzungsanalyse, Diagnostik und Konfiguration des ML Kit verarbeiten kann. Sie haben das Recht, Ihre Einwilligung jederzeit zu widerrufen, ohne dass die Rechtmäßigkeit der erfolgten Verarbeitung berührt wird. Das Ablehnen führt jedoch dazu, dass Sie den Rezeptcodescanner nicht verwenden können. - Einverstanden - Abbrechen Fehler 20 10 76631 Das Zertifikat Ihrer Gesundheitskarte ist ungültig. Ist Ihre Karte möglicherweise abgelaufen? Bitte kontaktieren Sie Ihre Krankenversicherung. Erfolglose Anmeldeversuche @@ -269,21 +238,11 @@ Bitte geben Sie einen Namen für das neue Profil ein. Profilname Profile - Profil hinzufügen - Gesundheitskarte - Krankenversicherung kontaktieren - Um sich in dieser App anmelden zu können, benötigen Sie eine NFC-fähige Gesundheitskarte sowie eine zugehörige PIN. - Diese erhalten Sie kostenfrei von Ihrer Krankenversicherung. Hierfür müssen Sie sich mittels amtlichem Ausweisdokument identifiziert haben. So erkennen Sie eine NFC-fähige Gesundheitskarte - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204 - Krankenversicherung wählen - Auswahl treffen - Was möchten Sie beantragen? Keine Kontaktaufnahme über diese App möglich Bitte nutzen Sie die üblichen Kanäle, um Ihre Versicherung zu kontaktieren. Gesundheitskarte & PIN Nur PIN - Kontaktieren Sie Ihre Krankenversicherung Anmeldung in der E-Rezept App Das Namensfeld darf nicht leer sein. Ein Profil mit dem eingegebenen Namen existiert bereits. @@ -320,18 +279,15 @@ Wir verwenden ML Kit von Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland (\"Google\"), um den Rezept-QR-Code zu lesen. Der Rezeptcode ist eine eindeutige Identifizierung Ihres Rezeptes. Er kann verglichen werden mit der Nummer eines Schließfachs. Um diesen Code komfortabel und schnell auszulesen, wird das ML Kit genutzt. Die Verarbeitung des Rezeptcodes findet ausschließlich auf Ihrem Gerät statt. \n\nBei dem ersten Starten des Rezeptcodescanners in unserer App, wird ML Kit auf Ihre Gerät heruntergeladen. Zu diesem Zweck erhebt Google Ihre IP-Adresse. Die Verarbeitung dient der Bereitstellung des Dienstes.\n\nDarüber hinaus erhebt Google folgende nicht-personenbezogene Informationen zum Zwecke der Nutzungsanalyse, Diagnostik und Konfiguration des ML Kit:\n - Geräteinformationen (z. B. Hersteller, Gerätemodell, Betriebssystemversion, Hardware, Mobilfunkbetreiber, Zeitzone und Spracheinstellungen)\n - Informationen über die Applikation (z.B. Version der App)\n - Informationen über die Konfiguration von ML Kit\n - Fehlermeldungen\n - Ereignistypen (initialisieren, Modell herunterladen, aktualisieren, ausführen, Erkennung)\n - Technische Leistungsdaten Ihres Gerätes\n - IP-Adresse (wird nur temporär gespeichert)\n - Weitere Daten, insbesondere Ihre Rezeptdaten, werden nicht von Google erhoben.\n\nDie Verarbeitung Ihrer Informationen wird nicht nur von Google Ireland Limited, sondern kann auch von Google LLC in den USA durchgeführt werden. Weiterführendes unter 7. Übermittlung in Drittländer. https://policies.google.com/privacy/frameworks https://support.google.com/policies/contact/general_privacy_form - Profil wählen - Profile bearbeiten Keine neuen Rezepte verfügbar - %s Rezept aktualisiert - %s Rezepte aktualisiert + %s neues Rezept + %s neue Rezepte Einlösbar In Einlösung Eingelöst Unbekannt - Details Zugriffsprotokolle anzeigen Wer hat wann auf Ihre Rezepte zugegriffen? Zugangsschlüssel zum Rezeptdienst @@ -382,9 +338,6 @@ Verbinden der Gesundheitskarte fehlgeschlagen Das aktuelle Profil ist bereits mit einer anderen Gesundheitskarte (Krankenversicherungsnummer %s) verbunden. Ihre Gesundheitskarte ist bereits mit einem anderen Profil verbunden. Wechseln Sie zu Profil %s. - Meine Bestellung - Jetzt reservieren - Jetzt bestellen Speichern Kontaktdaten und Adresse Kontakt @@ -400,21 +353,13 @@ PLZ und Ort Bitte geben Sie für die Kontaktaufnahme eine Postleitzahl und den Ort an. Lieferanweisung (optional) - Hiermit wird Ihr Rezept an diese Apotheke gesendet. Sie können es anschließend in keiner anderen Apotheke mehr einlösen. - Kontaktdaten und Lieferadresse - Rezepte - Wir benötigen Ihre Kontaktdaten zur Beratung durch die Apotheke und um Sie über den aktuellen Stand Ihrer Bestellung zu informieren. - Kontaktdaten angeben Weitere Kontaktdaten benötigt - Bestellung erfolgreich übermittelt - Ihre Apotheke wird sich bald mit Ihnen in Verbindung setzen. - Schließen Änderungen verwerfen? Verwerfen Für die Suche nutzt das Apothekenverzeichnis Geokoordinaten, die mit Hilfe von OpenStreetMap ermittelt wurden. Wir danken dem Projekt für diese Hilfe. © OpenStreetMap (%s) https://www.openstreetmap.org/copyright - Nutzung & Datenschutz + Datenschutz & Nutzung Weiter Ihre PIN haben Sie in einem Brief von Ihrer Krankenversicherung erhalten. Keine PIN erhalten @@ -464,21 +409,11 @@ Verbundene Geräte Registriert seit %s (dieses Gerät) Registriert seit %s - Aktuell - Archiv - Erneut einlösen? - Hinweis: Die Apotheke, die ein Rezept als erste akzeptiert, blockiert es für eine Bearbeitung durch eine weitere Apotheke. - Abbrechen - Okay - - Sie haben das Rezept %s bereits an eine Apotheke gesendet. Dennoch erneut einlösen? - Sie haben einige dieser Rezepte bereits an eine Apotheke gesendet. Dennoch an weitere Apotheke senden? - Aus Sicherheitsgründen wird die Verbindung zum Rezeptserver nach 12 Stunden getrennt. Für eine erneute Verbindung benötigen Sie für jeden Verbindungsvorgang Gesundheitskarte und PIN. PIN PIN (Gesundheitskarte) eingeben Weiter - Authentisierung + Anmelden Verbundene Geräte Gerät entfernen? Abbrechen @@ -500,8 +435,6 @@ Brauchen Sie Hilfe? Wir haben für Sie einige Tipps zusammengestellt, um die häufigsten Probleme zu lösen. Verbindungs-Tipps starten - Gescannt am: %s - Gescanntes Rezept Entsperren Karte gesperrt Die PIN wurde dreimal falsch eingegeben. Ihre Karte wurde daher aus Sicherheitsgründen gesperrt. @@ -531,10 +464,9 @@ PIN zur Gesundheitskarte Sie verfügen noch nicht über eine NFC-fähige Gesundheitskarte und PIN? Jetzt bestellen - https://www.das-e-rezept-fuer-deutschland.de/fragen-antworten/woran-erkenne-ich-ob-ich-eine-nfc-faehige-gesundheitskarte-habe#c204) Oder: Melden Sie sich mit der %s an. App Ihrer Krankenversicherung - "Ihre Zugangsnummer (Card Access Number, kurz: CAN) finden Sie in der rechten oberen Ecke Ihrer Gesundheitskarte." + "Ihre Zugangsnummer finden Sie in der rechten oberen Ecke Ihrer Gesundheitskarte." Meine Karte verfügt über keine Zugangsnummer Sie haben noch %s weiteren Versuch, bevor Ihre Karte gesperrt wird. @@ -553,10 +485,6 @@ im unteren Bereich mittig im unteren Bereich links Hilfe - Eingelöst vor %s Minuten - Eingelöst am %s - Eingelöst gerade eben - Eingelöst um %s Uhr Gesendet vor %s Minuten Gesendet am %s Gesendet gerade eben @@ -598,10 +526,9 @@ Name Versicherung Versichertennummer - Zugangsnummer (CAN) + Kartenzugangsnummer Anmelden Abmelden - Um Rezepte automatisch zu erhalten, müssen Sie angemeldet sein. Speichern Ändern Profilbild bearbeiten @@ -617,8 +544,7 @@ Keine Tokens Sie erhalten einen Token, wenn Sie am Rezeptdienst angemeldet sind.\n Bestellungen - Bestellungen - https://gematik.shortcm.li/E-Rezept-App_Feedback + https://t.maze.co/90489290 Wunsch-PIN wählen Karte entsperren PIN wählen @@ -644,19 +570,17 @@ Neu Verlauf Bestellung - Kostenfrei für den Anrufer. Servicezeiten: Mo - Fr 08:00 - 20:00 Uhr außer an bundeseinheitlichen Feiertagen + Kostenfrei für den Anrufenden. Servicezeiten: Mo - Fr 08:00 - 20:00 Uhr außer an bundeseinheitlichen Feiertagen Apotheke Wunsch-PIN wählen Wunsch-PIN gespeichert - Speichern der Wunsch-PIN nicht möglich E-Rezept Aktuell geöffnet und in meiner Nähe Filtern nach … Suche starten - Abbrechen - Wird für Sie eingelöst + Wurde für Sie eingelöst Direktzuweisung - Einlösen + Apotheken Telefonnummer (optional) Nach Name oder Adresse suchen Keine gültigen Apothekeninformationen @@ -674,13 +598,12 @@ Anmelden Profil 1 In meiner Nähe - Sie haben keine einlösbaren Rezepte Später einlösbar Einlösbar ab %s - %s/%s + %s / %s Produktverbesserungen Anonyme Analyse - Helfen Sie uns, diese App besser zu machen. Alle Nutzerdaten werden anonym erhoben und dienen ausschließlich der Verbesserung des Nutzungserlebsnisses. + Helfen Sie uns, diese App besser zu machen. Alle Nutzungsdaten werden anonym erhoben und dienen ausschließlich der Verbesserung des Nutzungserlebsnisses. Gerätesicherheit Persönliche Einstellungen Bedienungshilfen @@ -688,7 +611,7 @@ Rezept hinzugefügt Rezept bereits vorhanden Ein Fehler beim Importieren ist aufgetreten - CAN + Kartenzugangsnummer Löschen Gescanntes Rezept Ersatzpräparat möglich @@ -717,11 +640,8 @@ Nein Dosierung Ausstellungsdatum - Noctu Dieses Rezept wird im Rahmen einer Behandlung für Sie eingelöst. - Dieses Rezept wurde im Rahmen einer Behandlung für Sie eingelöst. Keine Angabe - Keine Notdienstgebühr Zuzahlung Medikament Abgabehinweise @@ -740,8 +660,6 @@ 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 - Dieses Rezept kann nachts in einer Apotheke nicht ohne zusätzliche Zahlung einer Notdienstgebühr eingelöst werden. Zuzahlungspflichtige Medikamente Von der Zuzahlung befreit Gesetzlich Versicherte müssen für verschreibungspflichtige Medikamente eine Zuzahlung von bis zu zehn Euro leisten.\n\nDie Höhe der Zuzahlung ist abhängig von dem Preis Ihres Medikaments. Medikamente, die weniger als 5€ kosten müssen Sie selbst zahlen.\nBei Medikamenten die teurer sind, müssen Sie zehn Prozent des Preises zuzahlen, mindestens aber 5€ und höchstens 10€.\n\nGrundsätzlich befreit von einer Zuzahlung sind Kinder und Jugendliche unter 18 Jahren. \n\nSollten Ihre jährlichen Kosten für Medikamente Ihre finanzielle Belastungsgrenzt überschreiten können Sie sich von der Zuzahlung befreien lassen. Sprechen Sie dazu mit Ihrer Krankenversicherung. @@ -796,15 +714,21 @@ Passwort nicht gefunden Es ist kein Passwort auf Ihrer Karte hinterlegt. Sie wurden abgemeldet - Melden Sie sich erneut an, um Ihre Rezepte zu aktualisieren. + Log in again to update your recipes. Wirkstoffnummer Wirkstärke und Einheit + Eingelöst vor %s Minuten + Eingelöst am %s + Eingelöst gerade eben + Eingelöst um %s Uhr + Bestellungen + Dieses Rezept wurde im Rahmen einer Behandlung für Sie eingelöst. + Notdienstgebühr + Dieses Rezept kann nachts in einer Apotheke nicht ohne zusätzliche Zahlung einer Notdienstgebühr eingelöst werden. Hier suchen - Kontakt und Öffnungszeiten Einstellungen Standort in den Einstellungen freigeben. In meiner Nähe - Profilnamen ändern Gedrückt halten, um den Namen zu bearbeiten. Geben Sie den neuen Namen für das Profil ein. Um Rezepte digital von Ihrer Praxis zu empfangen, müssen Sie angemeldet sein. @@ -814,11 +738,6 @@ Fügen Sie Rezepte über den + Button in der rechten oberen Ecke hinzu. Anmelden Eingelöste Rezepte - Zuletzt aktualisiert gerade eben - Zuletzt aktualisiert vor %s Minuten - Zuletzt aktualisiert vor %s Tagen - Zuletzt aktualisiert am %s - Zuletzt aktualisiert um %s Uhr Vielleicht später Anmelden Profilbild bearbeiten @@ -906,5 +825,6 @@ Ihre Versicherung bietet folgende Kontaktmöglichkeiten Ihre Versicherung bietet folgende Kontaktmöglichkeit Schließen - + Abrechnungen + Abrechnungen anzeigen diff --git a/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt b/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt index ff1ae4cd..06c5be48 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt @@ -36,7 +36,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.Instant @OptIn(ExperimentalCoroutinesApi::class) class MainViewModelTest { @@ -59,7 +58,6 @@ class MainViewModelTest { SettingsData.General( latestAppVersion = AppVersion(code = 1, name = "Test"), onboardingShownIn = null, - dataProtectionVersionAcceptedOn = Instant.now(), zoomEnabled = false, userHasAcceptedInsecureDevice = false, authenticationFails = 0, @@ -73,7 +71,6 @@ class MainViewModelTest { @Test fun `test showInsecureDevicePrompt - only show once`() = runTest { - every { settingsUseCase.showDataTermsUpdate } returns flowOf(false) every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(true) every { settingsUseCase.showOnboarding } returns flowOf(false) every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) @@ -87,7 +84,6 @@ class MainViewModelTest { @Test fun `test showInsecureDevicePrompt - device is secure`() = runTest { - every { settingsUseCase.showDataTermsUpdate } returns flowOf(false) every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(false) every { settingsUseCase.showOnboarding } returns flowOf(false) every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) @@ -98,30 +94,4 @@ class MainViewModelTest { assertEquals(false, viewModel.showInsecureDevicePrompt.first()) assertEquals(false, viewModel.showInsecureDevicePrompt.first()) } - - @Test - fun `test showDataTermsUpdate - dataTerms updates should be shown`() = runTest { - every { settingsUseCase.showDataTermsUpdate } returns flowOf(true) - every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(false) - every { settingsUseCase.showOnboarding } returns flowOf(false) - every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) - every { settingsUseCase.showMainScreenTooltip } returns flowOf(false) - - viewModel = MainViewModel(integrityUseCase, settingsUseCase) - - assertEquals(true, viewModel.showDataTermsUpdate.first()) - } - - @Test - fun `test showDataTermsUpdate - dataTerms updates should not be shown`() = runTest { - every { settingsUseCase.showDataTermsUpdate } returns flowOf(false) - every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(false) - every { settingsUseCase.showOnboarding } returns flowOf(false) - every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) - every { settingsUseCase.showMainScreenTooltip } returns flowOf(false) - - viewModel = MainViewModel(integrityUseCase, settingsUseCase) - - assertEquals(false, viewModel.showDataTermsUpdate.first()) - } } diff --git a/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt b/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt index eae82f62..88600e48 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt @@ -21,8 +21,8 @@ package de.gematik.ti.erp.app.orders.usecase import de.gematik.ti.erp.app.CoroutineTestRule import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.datetime.Instant import org.junit.Rule -import java.time.Instant import kotlin.test.Test import kotlin.test.assertEquals @@ -37,7 +37,7 @@ class OrderUseCaseTest { orderId = "", communicationId = "CID123456", profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", payload = """ @@ -52,7 +52,7 @@ class OrderUseCaseTest { ) val expected = OrderUseCaseData.Message( communicationId = "CID123456", - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), message = "Hi!", code = null, link = "https://example.org", @@ -68,7 +68,7 @@ class OrderUseCaseTest { orderId = "", communicationId = "CID123456", profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": " ", "pickUpCodeHR": "" }""", @@ -76,7 +76,7 @@ class OrderUseCaseTest { ) val expected = OrderUseCaseData.Message( communicationId = "CID123456", - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), message = null, code = null, link = null, @@ -92,7 +92,7 @@ class OrderUseCaseTest { orderId = "", communicationId = "CID123456", profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", payload = """{ - """, @@ -100,7 +100,7 @@ class OrderUseCaseTest { ) val expected = OrderUseCaseData.Message( communicationId = "CID123456", - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), message = null, code = null, link = null, @@ -116,7 +116,7 @@ class OrderUseCaseTest { orderId = "", communicationId = "CID123456", profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", payload = """{ "version": 1, "supplyOptionsType": "shipment", "url": "ftp://example.org" }""", @@ -124,7 +124,7 @@ class OrderUseCaseTest { ) val expected = OrderUseCaseData.Message( communicationId = "CID123456", - sentOn = Instant.ofEpochMilli(123456), + sentOn = Instant.fromEpochSeconds(123456), message = null, code = null, link = null, 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 fcea5191..23bc3255 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 @@ -35,10 +35,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.Instant import kotlin.test.assertEquals @ExperimentalCoroutinesApi @@ -54,7 +54,9 @@ class PharmacySearchViewModelTest { private val profile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), active = true, color = ProfilesData.ProfileColorNames.SPRING_GRAY, avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage, @@ -67,21 +69,21 @@ class PharmacySearchViewModelTest { taskId = "A", accessCode = "1234", title = "Test", - timestamp = Instant.EPOCH, + timestamp = Instant.fromEpochSeconds(0, 0), substitutionsAllowed = false ), PharmacyUseCaseData.PrescriptionOrder( taskId = "B", accessCode = "1234", title = "Test", - timestamp = Instant.EPOCH, + timestamp = Instant.fromEpochSeconds(0, 0), substitutionsAllowed = false ), PharmacyUseCaseData.PrescriptionOrder( taskId = "C", accessCode = "1234", title = "Test", - timestamp = Instant.EPOCH, + timestamp = Instant.fromEpochSeconds(0, 0), substitutionsAllowed = false ) ) diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt b/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt index 6342a000..f226a1a8 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt @@ -22,12 +22,13 @@ package de.gematik.ti.erp.app.prescription.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule import de.gematik.ti.erp.app.CoroutineTestRule +import kotlinx.datetime.Clock import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.Instant +@Suppress("UnusedPrivateMember") class TwoDCodeValidatorTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @@ -43,7 +44,7 @@ class TwoDCodeValidatorTest { " \"Task/234fabe0964598efd23f34dd23e122b2323344ea8e8934dae23e2a9a934513bc/\$accept?ac=777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea\"\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ) private val scannedTask3 = ScannedCode( @@ -54,7 +55,7 @@ class TwoDCodeValidatorTest { " \"Task/5e78f21cd6abc35edf4f1726c3d451ea2736d547a263f45726bc13a47e65d189/\$accept?ac=d3e6092ae3af14b5225e2ddbe5a4f59b3939a907d6fdd5ce6a760ca71f45d8e5\"\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ) private val scannedTask4 = ScannedCode( @@ -66,7 +67,7 @@ class TwoDCodeValidatorTest { " \"Task/5e78f21cd6abc35edf4f1726c3d451ea2736d547a263f45726bc13a47e65d189/\$accept?ac=d3e6092ae3af14b5225e2ddbe5a4f59b3939a907d6fdd5ce6a760ca71f45d8e5\"\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ) private val notWellFormatted = ScannedCode( @@ -76,7 +77,7 @@ class TwoDCodeValidatorTest { " \"Task/5e78f21cd6abc35edf4f1726c3d451ea2736d547a263f45726bc13a47e65d189/\$accept?ac=d3e6092ae3af14b5225e2ddbe5a4f59b3939a907d6fdd5ce6a760ca71f45d8e5\",\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ) private val emptyUrls = ScannedCode( @@ -84,7 +85,7 @@ class TwoDCodeValidatorTest { " \"urls\": [\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ) private val checkedTask1 = ValidScannedCode( @@ -94,7 +95,7 @@ class TwoDCodeValidatorTest { " \"Task/234fabe0964598efd23f34dd23e122b2323344ea8e8934dae23e2a9a934513bc/\$accept?ac=777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea\"\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ), mutableListOf( "Task/234fabe0964598efd23f34dd23e122b2323344ea8e8934dae23e2a9a934513bc/\$accept?ac=777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea" @@ -110,7 +111,7 @@ class TwoDCodeValidatorTest { " \"Task/5e78f21cd6abc35edf4f1726c3d451ea2736d547a263f45726bc13a47e65d189/\$accept?ac=d3e6092ae3af14b5225e2ddbe5a4f59b3939a907d6fdd5ce6a760ca71f45d8e5\"\n" + " ]\n" + "}", - Instant.now() + Clock.System.now() ), mutableListOf( "Task/234fabe0964598efd23f34dd23e122b2323344ea8e8934dae23e2a9a934513bc/\$accept?ac=777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea", diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt b/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt index 6e2e63ad..d46cb304 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt @@ -18,16 +18,17 @@ package de.gematik.ti.erp.app.prescription.ui.model -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes class SentOrCompletedTest { @Test fun `just now - completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(4) + val now = Clock.System.now() + val lastModified = now - 4.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = true) @@ -36,8 +37,8 @@ class SentOrCompletedTest { @Test fun `just now - not completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(4) + val now = Clock.System.now() + val lastModified = now - 4.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = false) @@ -46,8 +47,8 @@ class SentOrCompletedTest { @Test fun `redeemed minutes ago - completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(30) + val now = Clock.System.now() + val lastModified = now - 30.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = true) @@ -56,8 +57,8 @@ class SentOrCompletedTest { @Test fun `redeemed minutes ago - not completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(30) + val now = Clock.System.now() + val lastModified = now - 30.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = false) @@ -66,8 +67,8 @@ class SentOrCompletedTest { @Test fun `redeemed hours ago - completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(120) + val now = Clock.System.now() + val lastModified = now - 120.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = true) @@ -76,8 +77,8 @@ class SentOrCompletedTest { @Test fun `redeemed hours ago - not completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofMinutes(120) + val now = Clock.System.now() + val lastModified = now - 120.minutes val result = sentOrCompleted(lastModified = lastModified, now = now, completed = false) @@ -86,8 +87,8 @@ class SentOrCompletedTest { @Test fun `redeemed on - completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofDays(1) + val now = Clock.System.now() + val lastModified = now - 1.days val result = sentOrCompleted(lastModified = lastModified, now = now, completed = true) @@ -96,8 +97,8 @@ class SentOrCompletedTest { @Test fun `redeemed on - not completed`() { - val now = Instant.now() - val lastModified = now - Duration.ofDays(1) + val now = Clock.System.now() + val lastModified = now - 1.days val result = sentOrCompleted(lastModified = lastModified, now = now, completed = false) 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 dd158ade..bf267116 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 @@ -33,11 +33,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.LocalDate -import java.time.ZoneOffset import kotlin.test.assertEquals @ExperimentalCoroutinesApi @@ -68,7 +69,7 @@ class PrescriptionUseCaseTest { testSyncedTasksOrdered.map { it.taskId }, useCase.syncedActiveRecipes( profileId = "", - now = LocalDate.parse("2021-02-01").atStartOfDay().toInstant(ZoneOffset.UTC) + now = LocalDate.parse("2021-02-01").atStartOfDayIn(TimeZone.UTC) ).first().map { it.taskId } ) } @@ -89,7 +90,7 @@ class PrescriptionUseCaseTest { testRedeemedTaskIdsOrdered, useCase.redeemedPrescriptions( profileId = "", - now = LocalDate.parse("2021-02-01").atStartOfDay().toInstant(ZoneOffset.UTC) + now = LocalDate.parse("2021-02-01").atStartOfDayIn(TimeZone.UTC) ).first().map { it.taskId } ) } diff --git a/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt b/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt index f1edea83..6db76a2f 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import java.time.Instant +import kotlinx.datetime.Clock import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -58,7 +58,7 @@ class SettingsUseCaseTest { @Test fun `accept onboarding`() = runTest { - val now = Instant.now() + val now = Clock.System.now() settings.onboardingSucceeded( authenticationMode = SettingsData.AuthenticationMode.Unspecified, "Profil 1", @@ -74,55 +74,12 @@ class SettingsUseCaseTest { } } - @Test - fun `show data terms update - data accepted in the past`() = runTest { - every { settingsRepository.general } returns flowOf( - SettingsData.General( - latestAppVersion = AppVersion(code = 1, name = "Test"), - onboardingShownIn = null, - dataProtectionVersionAcceptedOn = DATA_PROTECTION_LAST_UPDATED.minusSeconds(1000), - zoomEnabled = false, - userHasAcceptedInsecureDevice = false, - authenticationFails = 0, - welcomeDrawerShown = false, - mainScreenTooltipsShown = true, - mlKitAccepted = false - ) - ) - - initSettings() - - assertEquals(true, settings.showDataTermsUpdate.first()) - } - - @Test - fun `show data terms update - data already accepted`() = runTest { - every { settingsRepository.general } returns flowOf( - SettingsData.General( - latestAppVersion = AppVersion(code = 1, name = "Test"), - onboardingShownIn = null, - dataProtectionVersionAcceptedOn = DATA_PROTECTION_LAST_UPDATED.plusSeconds(1000), - zoomEnabled = false, - userHasAcceptedInsecureDevice = false, - authenticationFails = 0, - welcomeDrawerShown = false, - mainScreenTooltipsShown = true, - mlKitAccepted = false - ) - ) - - initSettings() - - assertEquals(false, settings.showDataTermsUpdate.first()) - } - @Test fun `show welcome drawer`() = runTest { every { settingsRepository.general } returns flowOf( SettingsData.General( latestAppVersion = AppVersion(code = 1, name = "Test"), onboardingShownIn = null, - dataProtectionVersionAcceptedOn = DATA_PROTECTION_LAST_UPDATED.plusSeconds(1000), zoomEnabled = false, userHasAcceptedInsecureDevice = false, authenticationFails = 0, @@ -142,7 +99,6 @@ class SettingsUseCaseTest { SettingsData.General( latestAppVersion = AppVersion(code = 1, name = "Test"), onboardingShownIn = null, - dataProtectionVersionAcceptedOn = DATA_PROTECTION_LAST_UPDATED.plusSeconds(1000), zoomEnabled = false, userHasAcceptedInsecureDevice = false, authenticationFails = 0, diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt b/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt index d7661be4..fcc485cd 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt @@ -18,7 +18,7 @@ package de.gematik.ti.erp.app.utils -import de.gematik.ti.erp.app.fhir.parser.asTemporalAccessor +import de.gematik.ti.erp.app.fhir.parser.toFhirTemporal import org.junit.Test import java.util.Locale import kotlin.test.assertEquals @@ -28,10 +28,11 @@ class DateTimeTest { fun `format temporal accessor`() { Locale.setDefault(Locale.GERMAN) - assertEquals("07.02.2015, 11:28:17", temporalText("2015-02-07T13:28:17+02:00".asTemporalAccessor()!!)) - assertEquals("07.02.2015", temporalText("2015-02-07".asTemporalAccessor()!!)) - assertEquals("Februar 2015", temporalText("2015-02".asTemporalAccessor()!!)) - assertEquals("2015", temporalText("2015".asTemporalAccessor()!!)) - assertEquals("11:28:17", temporalText("11:28:17".asTemporalAccessor()!!)) + assertEquals("07.02.2015, 11:28:17", temporalText("2015-02-07T13:28:17+02:00".toFhirTemporal())) + assertEquals("07.02.2015", temporalText("2015-02-07".toFhirTemporal())) + // requires sdk >= O + // assertEquals("Februar 2015", temporalText("2015-02".toFhirTemporal())) + // assertEquals("2015", temporalText("2015".toFhirTemporal())) + assertEquals("11:28:17", temporalText("11:28:17".toFhirTemporal())) } } 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 533d73c4..5395d628 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 @@ -20,9 +20,9 @@ package de.gematik.ti.erp.app.utils import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import java.time.Duration -import java.time.Instant +import kotlinx.datetime.Instant import java.util.UUID +import kotlin.time.Duration.Companion.days fun syncedTask( taskId: String = "Task/" + UUID.randomUUID().toString(), @@ -122,8 +122,8 @@ val testSyncedTasks = lastModified = Instant.parse("2020-12-06T14:49:46Z"), organizationName = null, practitionerName = "Praxis Glücklicher gehts nicht", - expiresOn = Instant.parse("2020-12-02T14:49:46Z") + Duration.ofDays(3 * 28), - acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + Duration.ofDays(28), + expiresOn = Instant.parse("2020-12-02T14:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + 28.days, authoredOn = Instant.parse("2020-12-02T14:49:46Z"), status = SyncedTaskData.TaskStatus.Completed, medicationName = "Schokolade" @@ -133,8 +133,8 @@ val testSyncedTasks = lastModified = Instant.parse("2020-12-05T14:49:46Z"), organizationName = null, practitionerName = "Praxis Glücklicher gehts nicht", - expiresOn = Instant.parse("2020-12-02T22:49:46Z") + Duration.ofDays(3 * 28), - acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + Duration.ofDays(28), + expiresOn = Instant.parse("2020-12-02T22:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + 28.days, authoredOn = Instant.parse("2020-12-02T14:49:46Z"), status = SyncedTaskData.TaskStatus.Completed, medicationName = "Bonbons" @@ -144,8 +144,8 @@ val testSyncedTasks = lastModified = Instant.parse("2020-12-05T09:49:46Z"), organizationName = null, practitionerName = "Praxis Glücklicher gehts nicht", - expiresOn = Instant.parse("2020-12-02T14:49:46Z") + Duration.ofDays(3 * 28), - acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + Duration.ofDays(28), + expiresOn = Instant.parse("2020-12-02T14:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-02T14:49:46Z") + 28.days, authoredOn = Instant.parse("2020-12-05T09:49:46Z"), status = SyncedTaskData.TaskStatus.Ready, medicationName = "Gummibärchen" @@ -155,8 +155,8 @@ val testSyncedTasks = lastModified = Instant.parse("2020-12-20T09:49:46Z"), organizationName = "MVZ Haus der vielen Ärzte", practitionerName = null, - expiresOn = Instant.parse("2020-12-20T09:49:46Z") + Duration.ofDays(3 * 28), - acceptUntil = Instant.parse("2020-12-20T09:49:46Z") + Duration.ofDays(28), + expiresOn = Instant.parse("2020-12-20T09:49:46Z") + (3 * 28).days, + acceptUntil = Instant.parse("2020-12-20T09:49:46Z") + 28.days, authoredOn = Instant.parse("2020-12-20T09:49:46Z"), status = SyncedTaskData.TaskStatus.Ready, medicationName = "Viel zu viel" @@ -166,8 +166,8 @@ val testSyncedTasks = lastModified = Instant.parse("2020-12-04T09:49:46Z"), organizationName = "MVZ Haus der vielen Ärzte", practitionerName = null, - expiresOn = Instant.parse("2020-12-04T09:49:46Z") + Duration.ofDays(3 * 28), - acceptUntil = Instant.parse("2020-12-04T09:49:46Z") + Duration.ofDays(28), + 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.Ready, medicationName = "Viel zu viel" diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt b/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt index 5403ea26..d1bee3aa 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt @@ -22,13 +22,13 @@ package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList +import kotlinx.datetime.Instant import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okio.ByteString.Companion.decodeBase64 import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.SecureRandom -import java.time.Instant val BCProvider = BouncyCastleProvider() @@ -75,9 +75,9 @@ object TestCertificates { val CertList: UntrustedCertList by lazy { Json.decodeFromString(JsonCertList) } - val ValidTimestamp: Instant = Instant.ofEpochSecond(1615368104) // 2021-03-10T09:21:44.000Z + val ValidTimestamp: Instant = Instant.fromEpochSeconds(1615368104) // 2021-03-10T09:21:44.000Z val ExpiredTimestamp: Instant = - Instant.ofEpochSecond(1899364896) // 2030-03-10T09:21:36.812Z + Instant.fromEpochSeconds(1899364896) // 2030-03-10T09:21:36.812Z } object Idp1 { @@ -160,7 +160,7 @@ object TestCertificates { object OCSP1 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAxWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcDrUjkAJSNgAAYDzIwMjEwNTE3MDYyMzAxWqARGA8yMDIxMDUxNzA2MjMwMVqhIzAhMB8GCSsGAQUFBzABAgQSBBDkdyImUBsO+Q8iAA2xbXu8MAkGByqGSM49BAEDRwAwRAIgW+JlwUmnZCVsME2kOyQlcqF01Lel/0nQdE6IaZmFADECIGhOH1k5Dzq42y2jCxZCzxevRc6vY1o8ky0Xy4DxLIWJoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232581) // 2021-05-17T08:23:01.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232581) // 2021-05-17T08:23:01.000+0200 val CertToCheckSerialNumber = "1034953504625805" // IDP 1 object SignerCert { @@ -175,7 +175,7 @@ object TestCertificates { object OCSP2 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAxWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBuyypCGo7gAAYDzIwMjEwNTE3MDYyMzAxWqARGA8yMDIxMDUxNzA2MjMwMVqhIzAhMB8GCSsGAQUFBzABAgQSBBDIsivTG9WljP4InmqVdKQmMAkGByqGSM49BAEDRwAwRAIgZMCyRhqMOaEG10KPz3mL5Yh7oX9fiIdBl8WrxLT2SewCIEvjzedVlnbt/j4e7VALo2xl8wvOcYe8gT04+PqH5vkfoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232581) // 2021-05-17T08:23:01.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232581) // 2021-05-17T08:23:01.000+0200 val CertToCheckSerialNumber = "487275465566779" // IDP 2 } @@ -185,7 +185,7 @@ object TestCertificates { object OCSP3 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAwWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBPCti7yC3gAAYDzIwMjEwNTE3MDYyMzAwWqARGA8yMDIxMDUxNzA2MjMwMFqhIzAhMB8GCSsGAQUFBzABAgQSBBAWpjYsPzj/U96/S1MvypTWMAkGByqGSM49BAEDRwAwRAIgXfEC3h/1H2/aHGEyJY9L59S6NbqdkStBBk2vczj+3mwCIASMGDqPuhA7ZLBJ5HhHpwKYEQw/YPluyBMnz7j2dXtPoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232580) // 2021-05-17T08:23:00.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232580) // 2021-05-17T08:23:00.000+0200 val CertToCheckSerialNumber = "347632017809591" // VAU } diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt b/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt index 885b76ad..63b8d997 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt @@ -36,6 +36,8 @@ import io.mockk.coEvery import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.Interceptor @@ -46,10 +48,9 @@ import org.bouncycastle.cert.X509CertificateHolder import org.junit.Assume import org.junit.Rule import retrofit2.Retrofit -import java.time.Duration -import java.time.Instant import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.time.Duration @OptIn(ExperimentalCoroutinesApi::class) class TruststoreIntegrationTest { @@ -71,7 +72,6 @@ class TruststoreIntegrationTest { MockKAnnotations.init(this) } - @OptIn(ExperimentalSerializationApi::class) @Test fun `create truststore from remote source`() = runTest { Assume.assumeTrue(BuildKonfig.TEST_RUN_WITH_TRUSTSTORE_INTEGRATION) @@ -109,7 +109,7 @@ class TruststoreIntegrationTest { val useCase = TruststoreUseCase( TruststoreConfig { return@TruststoreConfig BuildKonfig.APP_TRUST_ANCHOR_BASE64 }, VauRepository(localDataSource, VauRemoteDataSource(vauService), coroutineRule.dispatchers), - { Instant.now() }, + { Clock.System.now() }, { untrustedOCSPList: UntrustedOCSPList, untrustedCertList: UntrustedCertList, trustAnchor: X509CertificateHolder, diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt b/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt index c6ac2c3a..62b4cd14 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt +++ b/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt @@ -52,8 +52,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import java.security.interfaces.ECPublicKey -import java.time.Duration import java.util.Base64 +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours @OptIn(ExperimentalCoroutinesApi::class) class TruststoreTest { @@ -85,16 +86,16 @@ class TruststoreTest { fun setup() { MockKAnnotations.init(this) - every { config.maxOCSPResponseAge } returns Duration.ofHours(12) + every { config.maxOCSPResponseAge } returns 12.hours every { config.trustAnchor } returns TestCertificates.RCA3.X509Certificate - every { timeSource() } returns ocspProducedAt + Duration.ofHours(2) + every { timeSource() } returns ocspProducedAt + 2.hours every { trustedTruststore.vauPublicKey } returns vauPublicKey every { trustedTruststore.idpCertificates } returns listOf(TestCertificates.Idp1.X509Certificate, TestCertificates.Idp2.X509Certificate) every { trustedTruststore.caCertificates } returns listOf(TestCertificates.CA10.X509Certificate) every { trustedTruststore.ocspResponses } returns TestCertificates.OCSP.OCSPList.responses.map { it.responseObject as BasicOCSPResp } - every { trustedTruststore.checkValidity(Duration.ofHours(12), ocspProducedAt) } coAnswers { } + every { trustedTruststore.checkValidity(12.hours, ocspProducedAt) } coAnswers { } coEvery { repository.invalidate() } coAnswers { } truststore = TruststoreUseCase( @@ -162,7 +163,7 @@ class TruststoreTest { findValidOcspResponses( ocspResps, listOf(certChain), - Duration.ofHours(12), + 12.hours, TestCertificates.OCSP2.ProducedAt ).toTypedArray() ) @@ -181,7 +182,7 @@ class TruststoreTest { findValidOcspResponses( ocspResps, listOf(certChain), - Duration.ofHours(12), + 12.hours, TestCertificates.OCSP2.ProducedAt ).toTypedArray() ) @@ -193,12 +194,12 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, TestCertificates.OCSP2.ProducedAt ) assertEquals(vauPublicKey, truststore.vauPublicKey) - truststore.checkValidity(Duration.ofHours(12), TestCertificates.OCSP2.ProducedAt) + truststore.checkValidity(12.hours, TestCertificates.OCSP2.ProducedAt) } @Test @@ -209,8 +210,8 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), - TestCertificates.OCSP2.ProducedAt + Duration.ofDays(1) + 12.hours, + TestCertificates.OCSP2.ProducedAt + 1.days ) false @@ -236,7 +237,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } returns trustedTruststore @@ -268,7 +269,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { @@ -310,7 +311,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { @@ -319,7 +320,7 @@ class TruststoreTest { every { trustedTruststore.checkValidity( - Duration.ofHours(12), + 12.hours, ocspProducedAt ) } throws IllegalStateException() @@ -366,7 +367,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, ocspProducedAt ) } throws IllegalStateException() @@ -406,7 +407,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { @@ -448,7 +449,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { @@ -485,7 +486,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { @@ -524,7 +525,7 @@ class TruststoreTest { TestCertificates.OCSP.OCSPList, TestCertificates.Vau.CertList, TestCertificates.RCA3.X509Certificate, - Duration.ofHours(12), + 12.hours, any() ) } answers { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 4abe5936..feeaa6b4 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,4 +1,5 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN +import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.INT import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.LONG import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING import de.gematik.ti.erp.app @@ -96,6 +97,7 @@ kotlin { } kotlinX { implementation(coroutines("core")) + implementation(datetime) } database { implementation(realm) @@ -261,6 +263,8 @@ buildkonfig { ocspResponseMaxAge: String ) { defaultConfigs(flavor) { + buildConfigField(STRING, "VERSION_NAME", VERSION_NAME) + buildConfigField(INT, "VERSION_CODE", VERSION_CODE) buildConfigField(BOOLEAN, "INTERNAL", isInternal.toString()) if (isInternal) { buildConfigField(STRING, "BASE_SERVICE_URI_PU", BASE_SERVICE_URI_PU) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallData.kt b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt similarity index 66% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallData.kt rename to common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt index 90c8cdb6..d2bc2f34 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallData.kt +++ b/common/src/androidMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonthAndroid.kt @@ -16,19 +16,15 @@ * */ -package de.gematik.ti.erp.app.cardwall.ui.model +package de.gematik.ti.erp.app.fhir.parser -import androidx.compose.runtime.Immutable +import android.os.Build +import androidx.annotation.RequiresApi -object CardWallData { - enum class AuthenticationMethod { - None, - Alternative, // e.g. biometrics - HealthCard - } +@RequiresApi(Build.VERSION_CODES.O) +fun YearMonth.toJavaYearMonth(): java.time.YearMonth = + java.time.YearMonth.of(this.year, this.monthNumber) - @Immutable - data class State( - val hardwareRequirementsFulfilled: Boolean - ) -} +@RequiresApi(Build.VERSION_CODES.O) +fun Year.toJavaYear(): java.time.Year = + java.time.Year.of(this.year) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt index b7416303..e2a2685d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt @@ -24,7 +24,8 @@ import io.github.aakira.napier.Napier import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import java.time.Instant +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -36,6 +37,8 @@ abstract class ResourcePaging( ) { private val lock = Mutex() + protected open val tag: String = "ResourcePaging" + protected suspend fun downloadPaged(profileId: ProfileIdentifier): Result = lock.withLock { withContext(dispatchers.IO) { @@ -62,7 +65,9 @@ abstract class ResourcePaging( count = maxPageSize ).fold( onSuccess = { - Napier.d { "Received ${it.count} entries" } + Napier.d { + "$tag - Received ${it.count} entries" + } if (it.count != maxPageSize) { condition = false } @@ -127,7 +132,8 @@ abstract class ResourcePaging( private fun toTimestampString(timestamp: Instant?) = timestamp?.let { - val tm = it.atOffset(ZoneOffset.UTC) + // TODO: remove java date time stuff + val tm = it.toJavaInstant().atOffset(ZoneOffset.UTC) .truncatedTo(ChronoUnit.SECONDS) .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt index cb281779..c154a362 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt @@ -23,6 +23,7 @@ import de.gematik.ti.erp.app.db.entities.v1.AuditEventEntityV1 import de.gematik.ti.erp.app.db.entities.v1.AvatarFigureV1 import de.gematik.ti.erp.app.db.entities.v1.IdpAuthenticationDataEntityV1 import de.gematik.ti.erp.app.db.entities.v1.IdpConfigurationEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.db.entities.v1.PasswordEntityV1 import de.gematik.ti.erp.app.db.entities.v1.pharmacy.PharmacyCacheEntityV1 import de.gematik.ti.erp.app.db.entities.v1.PharmacySearchEntityV1 @@ -49,7 +50,7 @@ import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 import io.realm.kotlin.ext.query -const val ACTUAL_SCHEMA_VERSION = 15L +const val ACTUAL_SCHEMA_VERSION = 17L val appSchemas = setOf( AppRealmSchema( @@ -121,6 +122,15 @@ val appSchemas = setOf( it.accidentType = AccidentTypeV1.None } } + if (migrationStartedFrom < 17L) { + query().find().forEach { + if (it.lastAuthenticated != null) { + it._insuranceType = InsuranceTypeV1.GKV.toString() + } else { + it._insuranceType = InsuranceTypeV1.None.toString() + } + } + } } ) ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt index 9e40bd5b..38583332 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverter.kt @@ -19,26 +19,31 @@ package de.gematik.ti.erp.app.db import io.realm.kotlin.types.RealmInstant -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset +import kotlinx.datetime.FixedOffsetTimeZone +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime -fun RealmInstant.toLocalDateTime(offset: ZoneOffset = ZoneOffset.UTC): LocalDateTime = - LocalDateTime.ofEpochSecond(epochSeconds, nanosecondsOfSecond, offset) +fun RealmInstant.toLocalDateTime(offset: FixedOffsetTimeZone = TimeZone.UTC): LocalDateTime = + toInstant().toLocalDateTime(offset) -fun LocalDateTime.toRealmInstant(offset: ZoneOffset = ZoneOffset.UTC) = - RealmInstant.from(toEpochSecond(offset), toLocalTime().nano) +fun LocalDateTime.toRealmInstant(offset: FixedOffsetTimeZone = TimeZone.UTC) = + toInstant(offset).let { + RealmInstant.from(it.epochSeconds, it.nanosecondsOfSecond) + } fun RealmInstant.toInstant(): Instant = when { - this == RealmInstant.MIN -> Instant.MIN - this == RealmInstant.MAX -> Instant.MAX - else -> Instant.ofEpochSecond(epochSeconds, nanosecondsOfSecond.toLong()) + this == RealmInstant.MIN -> Instant.DISTANT_FUTURE + this == RealmInstant.MAX -> Instant.DISTANT_PAST + else -> Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond.toLong()) } fun Instant.toRealmInstant() = when { - this == Instant.MIN -> RealmInstant.MIN - this == Instant.MAX -> RealmInstant.MAX - else -> RealmInstant.from(epochSecond, nano) + this == Instant.DISTANT_PAST -> RealmInstant.MIN + this == Instant.DISTANT_FUTURE -> RealmInstant.MAX + else -> RealmInstant.from(this.epochSeconds, this.nanosecondsOfSecond) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt index d183e296..3b88af1b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/Delegates.kt @@ -18,14 +18,13 @@ package de.gematik.ti.erp.app.db.entities -import de.gematik.ti.erp.app.fhir.parser.asFormattedString -import de.gematik.ti.erp.app.fhir.parser.asTemporalAccessor +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.toFhirTemporal import java.lang.IllegalArgumentException import kotlin.properties.ReadWriteProperty import kotlin.reflect.KMutableProperty import kotlin.reflect.KProperty import org.bouncycastle.util.encoders.Base64 -import java.time.temporal.TemporalAccessor inline fun > enumName(backingProperty: KMutableProperty) = object : ReadWriteProperty { @@ -72,11 +71,11 @@ fun byteArrayBase64Nullable(backingProperty: KMutableProperty) = } fun temporalAccessorNullable(backingProperty: KMutableProperty) = - object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): TemporalAccessor? = - backingProperty.getter.call()?.asTemporalAccessor() + object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): FhirTemporal? = + backingProperty.getter.call()?.toFhirTemporal() - override fun setValue(thisRef: Any?, property: KProperty<*>, value: TemporalAccessor?) { - backingProperty.setter.call(value?.asFormattedString()) + override fun setValue(thisRef: Any?, property: KProperty<*>, value: FhirTemporal?) { + backingProperty.setter.call(value?.formattedString()) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt index fc88dd3d..703f4575 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Profile.kt @@ -60,6 +60,12 @@ enum class AvatarFigureV1 { FemaleDeveloper } +enum class InsuranceTypeV1 { + GKV, + PKV, + None +} + class ProfileEntityV1 : RealmObject, Cascading { @PrimaryKey @@ -72,6 +78,11 @@ class ProfileEntityV1 : RealmObject, Cascading { @delegate:Ignore var avatarFigure: AvatarFigureV1 by enumName(::_avatarFigure) + var _insuranceType: String = InsuranceTypeV1.None.toString() + + @delegate:Ignore + var insuranceType: InsuranceTypeV1 by enumName(::_insuranceType) + var _colorName: String = ProfileColorNamesV1.SPRING_GRAY.toString() @delegate:Ignore diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt index cd40cb17..a1fcb522 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/Settings.kt @@ -27,7 +27,7 @@ import io.realm.kotlin.Deleteable import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore -import java.time.LocalDateTime +import kotlinx.datetime.LocalDateTime enum class SettingsAuthenticationMethodV1 { HealthCard, @@ -85,7 +85,7 @@ class SettingsEntityV1 : RealmObject, Cascading { var pharmacySearch: PharmacySearchEntityV1? = PharmacySearchEntityV1() var userHasAcceptedInsecureDevice: Boolean = false - var dataProtectionVersionAccepted: RealmInstant = LocalDateTime.of(2021, 10, 15, 0, 0).toRealmInstant() + var dataProtectionVersionAccepted: RealmInstant = LocalDateTime(2021, 10, 15, 0, 0).toRealmInstant() var password: PasswordEntityV1? = PasswordEntityV1() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt index 6af0e6e2..7f27275e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Medication.kt @@ -21,12 +21,12 @@ package de.gematik.ti.erp.app.db.entities.v1.task import de.gematik.ti.erp.app.db.entities.Cascading import de.gematik.ti.erp.app.db.entities.enumName import de.gematik.ti.erp.app.db.entities.temporalAccessorNullable +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal import io.realm.kotlin.Deleteable import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore import io.realm.kotlin.ext.realmListOf -import java.time.temporal.TemporalAccessor // https://simplifier.net/erezept/~resources?category=Profile&sortBy=RankScore_desc // BTM = Betäubungsmittel, AMVV = Arzneimittelverschreibungsverordnung @@ -34,6 +34,7 @@ enum class MedicationCategoryV1 { ARZNEI_UND_VERBAND_MITTEL, BTM, AMVV, + SONSTIGES, UNKNOWN } @@ -63,7 +64,7 @@ class MedicationEntityV1 : RealmObject, Cascading { var _expirationDate: String? = null @delegate:Ignore - var expirationDate: TemporalAccessor? by temporalAccessorNullable(::_expirationDate) + var expirationDate: FhirTemporal? by temporalAccessorNullable(::_expirationDate) var ingredients: RealmList = realmListOf() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt index 502371ea..f4204865 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/Patient.kt @@ -19,15 +19,21 @@ package de.gematik.ti.erp.app.db.entities.v1.task import de.gematik.ti.erp.app.db.entities.Cascading +import de.gematik.ti.erp.app.db.entities.temporalAccessorNullable import de.gematik.ti.erp.app.db.entities.v1.AddressEntityV1 +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal import io.realm.kotlin.Deleteable -import io.realm.kotlin.types.RealmInstant import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.Ignore class PatientEntityV1 : RealmObject, Cascading { var name: String? = null var address: AddressEntityV1? = null - var birthdate: RealmInstant? = null + + var _dateOfBirth: String? = null + + @delegate:Ignore + var dateOfBirth: FhirTemporal? by temporalAccessorNullable(::_dateOfBirth) var insuranceIdentifier: String? = null override fun objectsToFollow(): Iterator = diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapper.kt index b13dd8e3..f5b321ca 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapper.kt @@ -18,7 +18,8 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asInstant +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirInstant import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull import de.gematik.ti.erp.app.fhir.parser.containedString @@ -28,11 +29,10 @@ import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive -import java.time.Instant fun extractAuditEvents( bundle: JsonElement, - save: (id: String, taskId: String?, description: String, timestamp: Instant) -> Unit + save: (id: String, taskId: String?, description: String, timestamp: FhirTemporal.Instant) -> Unit ): Int { val bundleTotal = bundle.containedArrayOrNull("entry")?.size ?: 0 @@ -61,7 +61,7 @@ fun extractAuditEvents( fun extractAuditEvent( resource: JsonElement, - save: (id: String, taskId: String?, description: String, timestamp: Instant) -> Unit + save: (id: String, taskId: String?, description: String, timestamp: FhirTemporal.Instant) -> Unit ) { val id = resource.containedString("id") val text = resource.contained("text").containedString("div") @@ -71,7 +71,7 @@ fun extractAuditEvent( .firstOrNull() ?.containedString("value") - val timestamp = requireNotNull(resource.contained("recorded").jsonPrimitive.asInstant()) { + val timestamp = requireNotNull(resource.contained("recorded").jsonPrimitive.asFhirInstant()) { "Audit event field `recorded` missing" } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt index 9a8d36e5..6f29c94d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonPharmacyTimes.kt @@ -18,62 +18,63 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asLocalTime +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirLocalTime import kotlinx.serialization.json.JsonPrimitive -import java.time.LocalTime +import kotlinx.datetime.LocalTime -private val CommonPharmacyTimes: Map by lazy { +private val CommonPharmacyTimes: Map by lazy { mapOf( - "00:00:00" to LocalTime.of(0, 0), - "00:30:00" to LocalTime.of(0, 30), - "01:00:00" to LocalTime.of(1, 0), - "01:30:00" to LocalTime.of(1, 30), - "02:00:00" to LocalTime.of(2, 0), - "02:30:00" to LocalTime.of(2, 30), - "03:00:00" to LocalTime.of(3, 0), - "03:30:00" to LocalTime.of(3, 30), - "04:00:00" to LocalTime.of(4, 0), - "04:30:00" to LocalTime.of(4, 30), - "05:00:00" to LocalTime.of(5, 0), - "05:30:00" to LocalTime.of(5, 30), - "06:00:00" to LocalTime.of(6, 0), - "06:30:00" to LocalTime.of(6, 30), - "07:00:00" to LocalTime.of(7, 0), - "07:30:00" to LocalTime.of(7, 30), - "08:00:00" to LocalTime.of(8, 0), - "08:30:00" to LocalTime.of(8, 30), - "09:00:00" to LocalTime.of(9, 0), - "09:30:00" to LocalTime.of(9, 30), - "10:00:00" to LocalTime.of(10, 0), - "10:30:00" to LocalTime.of(10, 30), - "11:00:00" to LocalTime.of(11, 0), - "11:30:00" to LocalTime.of(11, 30), - "12:00:00" to LocalTime.of(12, 0), - "12:30:00" to LocalTime.of(12, 30), - "13:00:00" to LocalTime.of(13, 0), - "13:30:00" to LocalTime.of(13, 30), - "14:00:00" to LocalTime.of(14, 0), - "14:30:00" to LocalTime.of(14, 30), - "15:00:00" to LocalTime.of(15, 0), - "15:30:00" to LocalTime.of(15, 30), - "16:00:00" to LocalTime.of(16, 0), - "16:30:00" to LocalTime.of(16, 30), - "17:00:00" to LocalTime.of(17, 0), - "17:30:00" to LocalTime.of(17, 30), - "18:00:00" to LocalTime.of(18, 0), - "18:30:00" to LocalTime.of(18, 30), - "19:00:00" to LocalTime.of(19, 0), - "19:30:00" to LocalTime.of(19, 30), - "20:00:00" to LocalTime.of(20, 0), - "20:30:00" to LocalTime.of(20, 30), - "21:00:00" to LocalTime.of(21, 0), - "21:30:00" to LocalTime.of(21, 30), - "22:00:00" to LocalTime.of(22, 0), - "22:30:00" to LocalTime.of(22, 30), - "23:00:00" to LocalTime.of(23, 0), - "23:30:00" to LocalTime.of(23, 30) + "00:00:00" to FhirTemporal.LocalTime(LocalTime(0, 0, 0, 0)), + "00:30:00" to FhirTemporal.LocalTime(LocalTime(0, 30, 0, 0)), + "01:00:00" to FhirTemporal.LocalTime(LocalTime(1, 0, 0, 0)), + "01:30:00" to FhirTemporal.LocalTime(LocalTime(1, 30, 0, 0)), + "02:00:00" to FhirTemporal.LocalTime(LocalTime(2, 0, 0, 0)), + "02:30:00" to FhirTemporal.LocalTime(LocalTime(2, 30, 0, 0)), + "03:00:00" to FhirTemporal.LocalTime(LocalTime(3, 0, 0, 0)), + "03:30:00" to FhirTemporal.LocalTime(LocalTime(3, 30, 0, 0)), + "04:00:00" to FhirTemporal.LocalTime(LocalTime(4, 0, 0, 0)), + "04:30:00" to FhirTemporal.LocalTime(LocalTime(4, 30, 0, 0)), + "05:00:00" to FhirTemporal.LocalTime(LocalTime(5, 0, 0, 0)), + "05:30:00" to FhirTemporal.LocalTime(LocalTime(5, 30, 0, 0)), + "06:00:00" to FhirTemporal.LocalTime(LocalTime(6, 0, 0, 0)), + "06:30:00" to FhirTemporal.LocalTime(LocalTime(6, 30, 0, 0)), + "07:00:00" to FhirTemporal.LocalTime(LocalTime(7, 0, 0, 0)), + "07:30:00" to FhirTemporal.LocalTime(LocalTime(7, 30, 0, 0)), + "08:00:00" to FhirTemporal.LocalTime(LocalTime(8, 0, 0, 0)), + "08:30:00" to FhirTemporal.LocalTime(LocalTime(8, 30, 0, 0)), + "09:00:00" to FhirTemporal.LocalTime(LocalTime(9, 0, 0, 0)), + "09:30:00" to FhirTemporal.LocalTime(LocalTime(9, 30, 0, 0)), + "10:00:00" to FhirTemporal.LocalTime(LocalTime(10, 0, 0, 0)), + "10:30:00" to FhirTemporal.LocalTime(LocalTime(10, 30, 0, 0)), + "11:00:00" to FhirTemporal.LocalTime(LocalTime(11, 0, 0, 0)), + "11:30:00" to FhirTemporal.LocalTime(LocalTime(11, 30, 0, 0)), + "12:00:00" to FhirTemporal.LocalTime(LocalTime(12, 0, 0, 0)), + "12:30:00" to FhirTemporal.LocalTime(LocalTime(12, 30, 0, 0)), + "13:00:00" to FhirTemporal.LocalTime(LocalTime(13, 0, 0, 0)), + "13:30:00" to FhirTemporal.LocalTime(LocalTime(13, 30, 0, 0)), + "14:00:00" to FhirTemporal.LocalTime(LocalTime(14, 0, 0, 0)), + "14:30:00" to FhirTemporal.LocalTime(LocalTime(14, 30, 0, 0)), + "15:00:00" to FhirTemporal.LocalTime(LocalTime(15, 0, 0, 0)), + "15:30:00" to FhirTemporal.LocalTime(LocalTime(15, 30, 0, 0)), + "16:00:00" to FhirTemporal.LocalTime(LocalTime(16, 0, 0, 0)), + "16:30:00" to FhirTemporal.LocalTime(LocalTime(16, 30, 0, 0)), + "17:00:00" to FhirTemporal.LocalTime(LocalTime(17, 0, 0, 0)), + "17:30:00" to FhirTemporal.LocalTime(LocalTime(17, 30, 0, 0)), + "18:00:00" to FhirTemporal.LocalTime(LocalTime(18, 0, 0, 0)), + "18:30:00" to FhirTemporal.LocalTime(LocalTime(18, 30, 0, 0)), + "19:00:00" to FhirTemporal.LocalTime(LocalTime(19, 0, 0, 0)), + "19:30:00" to FhirTemporal.LocalTime(LocalTime(19, 30, 0, 0)), + "20:00:00" to FhirTemporal.LocalTime(LocalTime(20, 0, 0, 0)), + "20:30:00" to FhirTemporal.LocalTime(LocalTime(20, 30, 0, 0)), + "21:00:00" to FhirTemporal.LocalTime(LocalTime(21, 0, 0, 0)), + "21:30:00" to FhirTemporal.LocalTime(LocalTime(21, 30, 0, 0)), + "22:00:00" to FhirTemporal.LocalTime(LocalTime(22, 0, 0, 0)), + "22:30:00" to FhirTemporal.LocalTime(LocalTime(22, 30, 0, 0)), + "23:00:00" to FhirTemporal.LocalTime(LocalTime(23, 0, 0, 0)), + "23:30:00" to FhirTemporal.LocalTime(LocalTime(23, 30, 0, 0)) ) } -fun lookupTime(tm: JsonPrimitive?): LocalTime? = - tm?.let { CommonPharmacyTimes[it.content] ?: it.asLocalTime() } +fun lookupTime(tm: JsonPrimitive?): FhirTemporal.LocalTime? = + tm?.let { CommonPharmacyTimes[it.content] ?: it.asFhirLocalTime() } 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 e205c0d2..034adbc9 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,7 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asLocalDate +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 @@ -160,7 +160,7 @@ fun JsonElement.extractMultiplePresc val start = this.findAll("extension").filterWith("url", stringValue("Zeitraum")) .firstOrNull() ?.contained("valuePeriod") - ?.containedOrNull("start")?.jsonPrimitive?.asLocalDate() + ?.containedOrNull("start")?.jsonPrimitive?.asFhirLocalDate() 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 519f5f60..944044fc 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 @@ -18,7 +18,8 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asInstant +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirInstant import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull import de.gematik.ti.erp.app.fhir.parser.containedString @@ -32,7 +33,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonPrimitive -import java.time.Instant +import kotlinx.datetime.Instant private fun template( orderId: String, @@ -75,6 +76,47 @@ private fun template( } """.trimIndent() +// private fun templateVersion12( +// orderId: String, +// reference: String, +// payload: String, +// recipientTID: String +// ) = """ +// { +// "resourceType": "Communication", +// "meta": { +// "profile": [ +// "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_DispReq|1.2" +// ] +// }, +// "identifier": [ +// { +// "system": "https://gematik.de/fhir/NamingSystem/OrderID", +// "value": $orderId +// } +// ], +// "status": "unknown", +// "basedOn": [ +// { +// "reference": $reference +// } +// ], +// "recipient": [ +// { +// "identifier": { +// "system": "https://gematik.de/fhir/NamingSystem/TelematikID", +// "value": $recipientTID +// } +// } +// ], +// "payload": [ +// { +// "contentString": $payload +// } +// ] +// } +// """.trimIndent() + val json = Json { encodeDefaults = true prettyPrint = false @@ -90,6 +132,7 @@ fun createCommunicationDispenseRequest( val payloadString = json.encodeToString(payload) val reference = "Task/$taskId/\$accept?ac=$accessCode" + // Todo: use template Version 1.2 if supported val templateString = template( orderId = JsonPrimitive(orderId).toString(), reference = JsonPrimitive(reference).toString(), @@ -111,7 +154,7 @@ fun extractCommunications( communicationId: String, orderId: String?, profile: CommunicationProfile, - sentOn: Instant, + sentOn: FhirTemporal.Instant, sender: String, recipient: String, payload: String? @@ -131,12 +174,18 @@ fun extractCommunications( profileValue("https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq").invoke(profileString) -> CommunicationProfile.ErxCommunicationDispReq - profileValue("https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq", "1.1.1").invoke( + profileValue( + "https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq", + "1.1.1" + ).invoke( profileString ) -> CommunicationProfile.ErxCommunicationDispReq - profileValue("https://gematik.de/fhir/StructureDefinition/ErxCommunicationDispReq", "1.2").invoke( + profileValue( + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_DispReq", + "1.2" + ).invoke( profileString ) -> CommunicationProfile.ErxCommunicationDispReq @@ -173,7 +222,7 @@ fun extractCommunications( val communicationId = resource.containedString("id") - val sentOn = requireNotNull(resource.contained("sent").jsonPrimitive.asInstant()) { + val sentOn = requireNotNull(resource.contained("sent").jsonPrimitive.asFhirInstant()) { "Communication `sent` field missing" } 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 6b2d6c61..c4860b64 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 @@ -18,11 +18,10 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.isProfileValue import kotlinx.serialization.json.JsonElement -import java.time.LocalDate -import java.time.temporal.TemporalAccessor typealias AddressFn = ( line: List?, @@ -41,7 +40,7 @@ typealias OrganizationFn = ( typealias PatientFn = ( name: String?, address: Address, - birthDate: LocalDate?, + birthDate: FhirTemporal?, insuranceIdentifier: String? ) -> R @@ -57,7 +56,7 @@ typealias InsuranceInformationFn = ( ) -> R typealias MedicationRequestFn = ( - dateOfAccident: LocalDate?, + dateOfAccident: FhirTemporal.LocalDate?, location: String?, accidentType: AccidentType, emergencyFee: Boolean?, @@ -73,7 +72,7 @@ typealias MedicationRequestFn = ( typealias MultiplePrescriptionInfoFn = ( indicator: Boolean, numbering: Ratio?, - start: LocalDate? + start: FhirTemporal.LocalDate? ) -> R typealias MedicationFn = ( @@ -89,7 +88,7 @@ typealias MedicationFn = ( uniqueIdentifier: String?, ingredients: List, lotNumber: String?, - expirationDate: TemporalAccessor? + expirationDate: FhirTemporal? ) -> R typealias IngredientFn = ( @@ -114,6 +113,7 @@ enum class MedicationCategory { ARZNEI_UND_VERBAND_MITTEL, BTM, AMVV, + SONSTIGES, UNKNOWN } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt index 91667e02..d931d79e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapper.kt @@ -18,7 +18,8 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asLocalDate +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +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.containedBooleanOrNull @@ -28,7 +29,7 @@ import de.gematik.ti.erp.app.fhir.parser.containedStringOrNull import de.gematik.ti.erp.app.fhir.parser.isProfileValue import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive -import java.time.LocalDate +import kotlinx.datetime.LocalDate typealias MedicationDispenseFn = ( dispenseId: String, @@ -37,7 +38,7 @@ typealias MedicationDispenseFn = ( wasSubstituted: Boolean, dosageInstruction: String?, performer: String, // Telematik-ID - whenHandedOver: LocalDate + whenHandedOver: FhirTemporal.LocalDate ) -> R fun extractMedicationDispense( @@ -62,7 +63,7 @@ fun extractMedicat val dosageInstruction = resource.containedOrNull("dosageInstruction")?.containedStringOrNull("text") val performer = resource.containedArray("performer")[0] .contained("actor").contained("identifier").containedString("value") // Telematik-ID - val whenHandedOver = resource.contained("whenHandedOver").jsonPrimitive.asLocalDate() + val whenHandedOver = resource.contained("whenHandedOver").jsonPrimitive.asFhirLocalDate() ?: error("error on parsing date of delivery") return processMedicationDispense( 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 fbf8bfcc..2c2ee92a 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 @@ -36,8 +36,7 @@ import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.JsonElement import java.net.MalformedURLException import java.net.URL -import java.time.DayOfWeek -import java.time.LocalTime +import kotlinx.datetime.DayOfWeek val Contained = listOf("contained") val TypeCodingCode = listOf("type", "coding", "code") @@ -236,11 +235,8 @@ private fun openingHours( .asSequence() .flatMap { fhirHours -> (fhirHours as JsonObject).let { - val openingTime = lookupTime(fhirHours[startTimeAlias]?.jsonPrimitive) - ?: LocalTime.MIN - - val closingTime = lookupTime(fhirHours[endTimeAlias]?.jsonPrimitive) - ?: LocalTime.MAX + val openingTime = lookupTime(fhirHours[startTimeAlias]?.jsonPrimitive)?.value + val closingTime = lookupTime(fhirHours[endTimeAlias]?.jsonPrimitive)?.value val time = OpeningTime(openingTime = openingTime, closingTime = closingTime) @@ -265,8 +261,3 @@ private fun fhirDay(day: String) = "sun" -> DayOfWeek.SUNDAY else -> error("wrong day format: $day") } - -private fun openingHours(days: List, openingTime: LocalTime, closingTime: LocalTime) = - days.map { - it to OpeningTime(openingTime = openingTime, closingTime = closingTime) - } 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 b77e4ef0..6df6c1f2 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 @@ -18,9 +18,9 @@ package de.gematik.ti.erp.app.fhir.model -import java.time.DayOfWeek -import java.time.LocalTime -import java.time.OffsetDateTime +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime import kotlin.math.PI import kotlin.math.abs import kotlin.math.asin @@ -97,19 +97,23 @@ sealed interface PharmacyService interface TemporalPharmacyService : PharmacyService { val openingHours: OpeningHours - fun isOpenAt(tm: OffsetDateTime) = openingHours.isOpenAt(tm) + fun isOpenAt(tm: LocalDateTime) = openingHours.isOpenAt(tm) fun isAllDayOpen(day: DayOfWeek) = openingHours[day]?.any { it.isAllDayOpen() } ?: false - fun openUntil(tm: OffsetDateTime): LocalTime? { - val localTm = tm.toLocalTime() + fun openUntil(tm: LocalDateTime): LocalTime? { + val localTm = tm.time return openingHours[tm.dayOfWeek]?.find { it.isOpenAt(localTm) }?.closingTime } - fun opensAt(tm: OffsetDateTime): LocalTime? { - val localTm = tm.toLocalTime() + fun opensAt(tm: LocalDateTime): LocalTime? { + val localTm = tm.time return openingHours[tm.dayOfWeek]?.find { - it.openingTime >= localTm + if (it.openingTime == null) { + true + } else { + it.openingTime >= localTm + } }?.openingTime } } @@ -141,17 +145,25 @@ data class OpeningHours(val openingTime: Map>) : Map> by openingTime data class OpeningTime( - val openingTime: LocalTime, - val closingTime: LocalTime + val openingTime: LocalTime?, + val closingTime: LocalTime? ) { - fun isOpenAt(tm: LocalTime) = tm in openingTime..closingTime - fun isAllDayOpen() = openingTime == LocalTime.MIN && closingTime == LocalTime.MAX + fun isOpenAt(tm: LocalTime) = + when { + openingTime == null && closingTime != null -> tm <= closingTime + openingTime != null && closingTime == null -> tm >= openingTime + openingTime == null && closingTime == null -> true + openingTime != null && closingTime != null -> tm in openingTime..closingTime + else -> error("Unreachable") + } + + fun isAllDayOpen() = openingTime == null && closingTime == null } -fun OpeningHours.isOpenAt(tm: OffsetDateTime) = +fun OpeningHours.isOpenAt(tm: LocalDateTime) = get(tm.dayOfWeek)?.any { - it.isOpenAt(tm.toLocalTime()) + it.isOpenAt(tm.time) } ?: false -fun Map.Entry>.isOpenToday(tm: OffsetDateTime) = +fun Map.Entry>.isOpenToday(tm: LocalDateTime) = key == tm.dayOfWeek && value.isNotEmpty() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt index 25b7bf91..124a0a1b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt @@ -18,8 +18,7 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asLocalDate -import de.gematik.ti.erp.app.fhir.parser.asTemporalAccessor +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.containedBoolean import de.gematik.ti.erp.app.fhir.parser.containedInt @@ -30,10 +29,9 @@ import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue -import io.github.aakira.napier.Napier +import de.gematik.ti.erp.app.fhir.parser.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive -import java.time.format.DateTimeParseException @Suppress("LongParameterList", "LongMethod") fun extractMedica val dateOfAccident = accidentInformation?.findAll("extension")?.filterWith( "url", stringValue("unfalltag") - )?.firstOrNull()?.containedOrNull("valueDate")?.jsonPrimitive?.asLocalDate() + )?.firstOrNull()?.containedOrNull("valueDate")?.jsonPrimitive?.asFhirLocalDate() val location = accidentInformation?.findAll("extension")?.filterWith( "url", @@ -301,12 +299,7 @@ fun extractPatient( ): Patient { val name = resource.extractHumanName() - val birthDate = try { - resource.containedOrNull("birthDate")?.jsonPrimitive?.asLocalDate() - } catch (expected: DateTimeParseException) { - Napier.e("Could not parse birthdate.", expected) - null - } + val birthDate = resource.containedOrNull("birthDate")?.jsonPrimitive?.toFhirTemporal() val kvnr = resource .findAll("identifier") @@ -367,7 +360,7 @@ fun extractPZNMedication( val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -427,7 +420,7 @@ fun extractMedicationCompounding( val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -467,7 +460,7 @@ fun extractMedicationFreetext( val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -522,7 +515,7 @@ fun extractMedicationIngredient( val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt index 024110e7..c02c9c89 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt @@ -18,8 +18,7 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asLocalDate -import de.gematik.ti.erp.app.fhir.parser.asTemporalAccessor +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.containedBoolean import de.gematik.ti.erp.app.fhir.parser.containedInt @@ -30,10 +29,9 @@ import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue import de.gematik.ti.erp.app.fhir.parser.stringValue -import io.github.aakira.napier.Napier +import de.gematik.ti.erp.app.fhir.parser.toFhirTemporal import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive -import java.time.format.DateTimeParseException @Suppress("LongParameterList", "LongMethod") fun extractMedica val dateOfAccident = accidentInformation?.findAll("extension")?.filterWith( "url", stringValue("unfalltag") - )?.firstOrNull()?.containedOrNull("valueDate")?.jsonPrimitive?.asLocalDate() + )?.firstOrNull()?.containedOrNull("valueDate")?.jsonPrimitive?.asFhirLocalDate() val location = accidentInformation?.findAll("extension")?.filterWith( "url", @@ -302,12 +300,7 @@ fun extractPatientVersion110( ): Patient { val name = resource.extractHumanName() - val birthDate = try { - resource.containedOrNull("birthDate")?.jsonPrimitive?.asLocalDate() - } catch (expected: DateTimeParseException) { - Napier.e("Could not parse birthdate.", expected) - null - } + val birthDate = resource.containedOrNull("birthDate")?.jsonPrimitive?.toFhirTemporal() val kvnr = resource .findAll("identifier") @@ -368,7 +361,7 @@ fun extractPZNMedicationVersion110( val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -429,7 +422,7 @@ fun extractMedicationCompoundingVersio val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -484,7 +477,7 @@ fun extractMedicationIngredientVersion val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -524,7 +517,7 @@ fun extractMedicationFreetextVersion11 val lotNumber = resource.containedOrNull("batch")?.containedStringOrNull("lotNumber") val expirationDate = resource.containedOrNull("batch") - ?.containedOrNull("expirationDate")?.jsonPrimitive?.asTemporalAccessor() + ?.containedOrNull("expirationDate")?.jsonPrimitive?.toFhirTemporal() return processMedication( text, @@ -579,6 +572,7 @@ private fun extractMedicationCategoryVerion110(resource: JsonElement): Medicatio "00" -> MedicationCategory.ARZNEI_UND_VERBAND_MITTEL "01" -> MedicationCategory.BTM "02" -> MedicationCategory.AMVV + "03" -> MedicationCategory.SONSTIGES else -> MedicationCategory.UNKNOWN } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt index 51c51762..b053ca5f 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapper.kt @@ -18,20 +18,20 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.fhir.parser.asInstant -import de.gematik.ti.erp.app.fhir.parser.asLocalDate +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirInstant +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.containedArrayOrNull import de.gematik.ti.erp.app.fhir.parser.containedString import de.gematik.ti.erp.app.fhir.parser.filterWith import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.fhir.parser.isProfileValue -import de.gematik.ti.erp.app.fhir.parser.profileValue import de.gematik.ti.erp.app.fhir.parser.stringValue import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.jsonPrimitive -import java.time.Instant -import java.time.LocalDate +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate enum class TaskStatus { Ready, @@ -61,19 +61,28 @@ fun extractTaskIds( .contained("profile") .contained() - if ( - profileValue( - "https://gematik.de/fhir/StructureDefinition/ErxTask", - "1.1.1" - ).invoke(profileString) - ) { - resource - .findAll("identifier") - .filterWith("system", stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID")) - .first() - .containedString("value") - } else { - null + when { + profileString.isProfileValue("https://gematik.de/fhir/StructureDefinition/ErxTask", "1.1.1") -> + resource + .findAll("identifier") + .filterWith("system", stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID")) + .first() + .containedString("value") + + profileString.isProfileValue( + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task", + "1.2" + ) -> + resource + .findAll("identifier") + .filterWith( + "system", + stringValue("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId") + ) + .first() + .containedString("value") + + else -> null } } @@ -130,10 +139,10 @@ fun extractTask( process: ( taskId: String, accessCode: String?, - lastModified: Instant, - expiresOn: LocalDate?, - acceptUntil: LocalDate?, - authoredOn: Instant, + lastModified: FhirTemporal.Instant, + expiresOn: FhirTemporal.LocalDate?, + acceptUntil: FhirTemporal.LocalDate?, + authoredOn: FhirTemporal.Instant, status: TaskStatus ) -> Unit ) { @@ -164,10 +173,10 @@ fun extractTaskVersion111( process: ( taskId: String, accessCode: String?, - lastModified: Instant, - expiresOn: LocalDate?, - acceptUntil: LocalDate?, - authoredOn: Instant, + lastModified: FhirTemporal.Instant, + expiresOn: FhirTemporal.LocalDate?, + acceptUntil: FhirTemporal.LocalDate?, + authoredOn: FhirTemporal.Instant, status: TaskStatus ) -> Unit ) { @@ -185,10 +194,10 @@ fun extractTaskVersion111( val status = mapTaskstatus(task.containedString("status")) - val authoredOn = requireNotNull(task.contained("authoredOn").jsonPrimitive.asInstant()) { + val authoredOn = requireNotNull(task.contained("authoredOn").jsonPrimitive.asFhirInstant()) { "Couldn't parse `authoredOn`" } - val lastModified = requireNotNull(task.contained("lastModified").jsonPrimitive.asInstant()) { + val lastModified = requireNotNull(task.contained("lastModified").jsonPrimitive.asFhirInstant()) { "Couldn't parse `lastModified`" } @@ -197,14 +206,14 @@ fun extractTaskVersion111( .filterWith("url", stringValue("https://gematik.de/fhir/StructureDefinition/ExpiryDate")) .first() .contained("valueDate") - .jsonPrimitive.asLocalDate() + .jsonPrimitive.asFhirLocalDate() val acceptUntil = task .findAll("extension") .filterWith("url", stringValue("https://gematik.de/fhir/StructureDefinition/AcceptDate")) .first() .contained("valueDate") - .jsonPrimitive.asLocalDate() + .jsonPrimitive.asFhirLocalDate() process( taskId, @@ -222,10 +231,10 @@ fun extractTaskVersion12( process: ( taskId: String, accessCode: String?, - lastModified: Instant, - expiresOn: LocalDate?, - acceptUntil: LocalDate?, - authoredOn: Instant, + lastModified: FhirTemporal.Instant, + expiresOn: FhirTemporal.LocalDate?, + acceptUntil: FhirTemporal.LocalDate?, + authoredOn: FhirTemporal.Instant, status: TaskStatus ) -> Unit ) { @@ -243,10 +252,10 @@ fun extractTaskVersion12( val status = mapTaskstatus(task.containedString("status")) - val authoredOn = requireNotNull(task.contained("authoredOn").jsonPrimitive.asInstant()) { + val authoredOn = requireNotNull(task.contained("authoredOn").jsonPrimitive.asFhirInstant()) { "Couldn't parse `authoredOn`" } - val lastModified = requireNotNull(task.contained("lastModified").jsonPrimitive.asInstant()) { + val lastModified = requireNotNull(task.contained("lastModified").jsonPrimitive.asFhirInstant()) { "Couldn't parse `lastModified`" } @@ -255,14 +264,14 @@ fun extractTaskVersion12( .filterWith("url", stringValue("https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate")) .first() .contained("valueDate") - .jsonPrimitive.asLocalDate() + .jsonPrimitive.asFhirLocalDate() val acceptUntil = task .findAll("extension") .filterWith("url", stringValue("https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate")) .first() .contained("valueDate") - .jsonPrimitive.asLocalDate() + .jsonPrimitive.asFhirLocalDate() process( taskId, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt index 0e9191d4..bf9d13a3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/Converter.kt @@ -34,87 +34,6 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.Year -import java.time.YearMonth -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.time.temporal.TemporalAccessor - -/** - * The Fhir documentation mentions the following formats: - * - * instant YYYY-MM-DDThh:mm:ss.sss+zz:zz - * datetime YYYY, YYYY-MM, YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+zz:zz - * date YYYY, YYYY-MM, or YYYY-MM-DD - * time hh:mm:ss - * - */ - -private const val DtFormatterPattern = "[HH:mm:ss][yyyy[-MM[-dd]]['T'HH:mm:ss[.SSS]XXX]]" -private const val DtFormatterPatternDateTime = "yyyy[-MM[-dd]]'T'HH:mm:ss[XXX]" -private const val DtFormatterPatternTime = "HH:mm:ss" - -private val Formatter = DateTimeFormatter.ofPattern(DtFormatterPattern) -private val FormatterDateTime = DateTimeFormatter.ofPattern(DtFormatterPatternDateTime) -private val FormatterTime = DateTimeFormatter.ofPattern(DtFormatterPatternTime) - -fun String?.asTemporalAccessor(): TemporalAccessor? = - this?.let { - Formatter - .parseBest( - it, - Instant::from, - LocalDate::from, - YearMonth::from, - Year::from, - LocalTime::from - ) - } - -fun TemporalAccessor?.asFormattedString(): String? = - when (this) { - is Instant -> this.toString() - is LocalDateTime -> FormatterDateTime.withZone(ZoneOffset.UTC).format(this) - is TemporalAccessor -> Formatter.withZone(ZoneOffset.UTC).format(this) - else -> null - } - -fun JsonPrimitive.asTemporalAccessor(): TemporalAccessor? = - this.contentOrNull?.let { - Formatter - .parseBest( - it, - Instant::from, - LocalDate::from, - YearMonth::from, - Year::from, - LocalTime::from - ) - } - -fun JsonPrimitive.asLocalTime(): LocalTime? = - this.contentOrNull?.let { - FormatterTime.parse(it, LocalTime::from) - } - -fun JsonPrimitive.asLocalDateTime(): LocalDateTime? = - this.contentOrNull?.let { - Formatter.parse(it, LocalDateTime::from) - } - -fun JsonPrimitive.asLocalDate(): LocalDate? = - this.contentOrNull?.let { - Formatter.parse(it, LocalDate::from) - } - -fun JsonPrimitive.asInstant(): Instant? = - this.contentOrNull?.let { - Formatter.parse(it, Instant::from) - } /** * Returns the first element in the JSON structure. For arrays this is the first element. diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverter.kt new file mode 100644 index 00000000..076e0427 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverter.kt @@ -0,0 +1,137 @@ +/* + * 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.fhir.parser + +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlin.jvm.JvmInline + +/** + * The Fhir documentation mentions the following formats: + * + * instant YYYY-MM-DDThh:mm:ss.sss+zz:zz + * datetime YYYY, YYYY-MM, YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+zz:zz + * date YYYY, YYYY-MM, or YYYY-MM-DD + * time hh:mm:ss + * + */ + +// keep the regex - but we can't be sure about all the different patterns +// val FhirInstantRegex = """(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d:\d\d)(\.\d\d\d)?(([+-]\d\d:\d\d)|Z)""".toRegex() +// val FhirLocalDateTimeRegex = """(\d\d\d\d-\d\d-\d\d)T(\d\d:\d\d(:\d\d)?)(\.\d\d\d)?""".toRegex() +// val FhirLocalDateRegex = """(\d\d\d\d-\d\d-\d\d)""".toRegex() +val FhirYearMonthRegex = """(?\d\d\d\d)-(?\d\d)""".toRegex() +val FhirYearRegex = """(?\d\d\d\d)""".toRegex() +// val FhirLocalTimeRegex = """(\d\d:\d\d(:\d\d)?)""".toRegex() + +sealed interface FhirTemporal { + @JvmInline + value class Instant(val value: kotlinx.datetime.Instant) : FhirTemporal + + @JvmInline + value class LocalDateTime(val value: kotlinx.datetime.LocalDateTime) : FhirTemporal + + @JvmInline + value class LocalDate(val value: kotlinx.datetime.LocalDate) : FhirTemporal + + @JvmInline + value class YearMonth(val value: de.gematik.ti.erp.app.fhir.parser.YearMonth) : FhirTemporal + + @JvmInline + value class Year(val value: de.gematik.ti.erp.app.fhir.parser.Year) : FhirTemporal + + @JvmInline + value class LocalTime(val value: kotlinx.datetime.LocalTime) : FhirTemporal + + fun formattedString(): String = + when (this) { + is Instant -> this.value.toString() + is LocalDate -> this.value.toString() + is LocalDateTime -> this.value.toString() + is LocalTime -> this.value.toString() + is Year -> this.value.toString() + is YearMonth -> this.value.toString() + } +} + +fun Instant.asFhirTemporal() = FhirTemporal.Instant(this) +fun LocalDateTime.asFhirTemporal() = FhirTemporal.LocalDateTime(this) +fun LocalDate.asFhirTemporal() = FhirTemporal.LocalDate(this) +fun YearMonth.asFhirTemporal() = FhirTemporal.YearMonth(this) +fun Year.asFhirTemporal() = FhirTemporal.Year(this) +fun LocalTime.asFhirTemporal() = FhirTemporal.LocalTime(this) + +@Suppress("ReturnCount") +fun String.toFhirTemporal(): FhirTemporal { + // going from the most specific to the least + + try { + return FhirTemporal.Instant(Instant.parse(this)) + } catch (_: IllegalArgumentException) { + } + try { + return FhirTemporal.LocalDateTime(LocalDateTime.parse(this)) + } catch (_: IllegalArgumentException) { + } + try { + return FhirTemporal.LocalDate(LocalDate.parse(this)) + } catch (_: IllegalArgumentException) { + } + try { + return FhirTemporal.YearMonth(YearMonth.parse(this)) + } catch (_: IllegalArgumentException) { + } + try { + return FhirTemporal.Year(Year.parse(this)) + } catch (_: IllegalArgumentException) { + } + try { + return FhirTemporal.LocalTime(LocalTime.parse(this)) + } catch (_: IllegalArgumentException) { + } + + error("Couldn't parse `$this`") +} + +fun JsonPrimitive.toFhirTemporal() = + this.contentOrNull?.toFhirTemporal() + +fun JsonPrimitive.asFhirLocalTime(): FhirTemporal.LocalTime? = + this.contentOrNull?.let { + FhirTemporal.LocalTime(LocalTime.parse(it)) + } + +fun JsonPrimitive.asLocalDateTime(): FhirTemporal.LocalDateTime? = + this.contentOrNull?.let { + FhirTemporal.LocalDateTime(LocalDateTime.parse(it)) + } + +fun JsonPrimitive.asFhirLocalDate(): FhirTemporal.LocalDate? = + this.contentOrNull?.let { + FhirTemporal.LocalDate(LocalDate.parse(it)) + } + +fun JsonPrimitive.asFhirInstant(): FhirTemporal.Instant? = + this.contentOrNull?.let { + FhirTemporal.Instant(Instant.parse(it)) + } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt new file mode 100644 index 00000000..eb52d74b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/parser/YearMonth.kt @@ -0,0 +1,68 @@ +/* + * 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.fhir.parser + +// just support sane values +private const val YearMin = 1000 +private const val YearMax = 9999 +private const val MonthMin = 1 +private const val MonthMax = 12 + +data class YearMonth(val year: Int, val monthNumber: Int) { + init { + require(year in YearMin..YearMax) + require(monthNumber in MonthMin..MonthMax) + } + + override fun toString(): String = "%d-%02d".format(year, monthNumber) + + companion object { + fun parse(value: String) = + requireNotNull( + FhirYearMonthRegex.matchEntire(value)?.let { + val year = requireNotNull(it.groups["year"]) { "`$value` missing field `year`" } + val month = requireNotNull(it.groups["month"]) { "`$value` missing field `month`" } + YearMonth( + year = year.value.toInt(), + monthNumber = month.value.toInt() + ) + } + ) { "`$value` doesn't match the pattern `YYYY-MM`" } + } +} + +data class Year(val year: Int) { + init { + require(year in YearMin..YearMax) + } + + override fun toString(): String = "%d".format(year) + + companion object { + fun parse(value: String) = + requireNotNull( + FhirYearRegex.matchEntire(value)?.let { + val year = requireNotNull(it.groups["year"]) { "`$value` missing field `year`" } + Year( + year = year.value.toInt() + ) + } + ) { "`$value` doesn't match the pattern `YYYY`" } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt index a20234a3..bd8cc0f5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/model/IdpData.kt @@ -18,11 +18,12 @@ package de.gematik.ti.erp.app.idp.model +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder import org.jose4j.base64url.Base64Url import org.jose4j.jwx.JsonWebStructure -import java.time.Duration -import java.time.Instant +import kotlin.time.Duration.Companion.hours object IdpData { data class IdpConfiguration( @@ -45,7 +46,7 @@ object IdpData { val expiresOn: Instant = extractExpirationTimestamp(token), val validOn: Instant = extractValidOnTimestamp(token) ) { - fun isValid(instant: Instant = Instant.now()) = + fun isValid(instant: Instant = Clock.System.now()) = instant < expiresOn && instant >= validOn } @@ -174,7 +175,7 @@ object IdpData { } fun extractExpirationTimestamp(ssoToken: String): Instant = - Instant.ofEpochSecond( + Instant.fromEpochSeconds( JsonWebStructure .fromCompactSerialization(ssoToken) .headers @@ -182,4 +183,4 @@ fun extractExpirationTimestamp(ssoToken: String): Instant = ) fun extractValidOnTimestamp(ssoToken: String): Instant = - extractExpirationTimestamp(ssoToken) - Duration.ofHours(24) + extractExpirationTimestamp(ssoToken) - 24.hours diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt index a82f9a6b..701c65d2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepository.kt @@ -38,13 +38,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update +import kotlinx.datetime.Instant import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.bouncycastle.cert.X509CertificateHolder import org.jose4j.base64url.Base64 import org.jose4j.jws.JsonWebSignature import java.security.PublicKey -import java.time.Instant @JvmInline value class JWSDiscoveryDocument(val jws: JsonWebSignature) @@ -163,8 +163,8 @@ class IdpRepository constructor( authenticationEndpoint = overwriteEndpoint(discoveryDocumentBody.authenticationURL), pukIdpEncEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpEnc), pukIdpSigEndpoint = overwriteEndpoint(discoveryDocumentBody.uriPukIdpSig), - expirationTimestamp = Instant.ofEpochSecond(discoveryDocumentBody.expirationTime), - issueTimestamp = Instant.ofEpochSecond(discoveryDocumentBody.issuedAt), + expirationTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.expirationTime), + issueTimestamp = Instant.fromEpochSeconds(discoveryDocumentBody.issuedAt), certificate = certificateHolder, externalAuthorizationIDsEndpoint = overwriteEndpoint(discoveryDocumentBody.krankenkassenAppURL), thirdPartyAuthorizationEndpoint = overwriteEndpoint(discoveryDocumentBody.thirdPartyAuthorizationURL) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt index 25faeaf7..1e5b45ec 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt @@ -46,8 +46,6 @@ import java.security.MessageDigest import java.security.PublicKey import java.security.Security import java.security.interfaces.ECPublicKey -import java.time.Duration -import java.time.Instant import javax.crypto.SecretKey import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -63,13 +61,16 @@ import org.jose4j.jwt.consumer.JwtContext import org.jose4j.jwt.consumer.NumericDateValidator import org.jose4j.jwx.JsonWebStructure import io.github.aakira.napier.Napier +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long +import kotlin.time.Duration.Companion.hours -private val discoveryDocumentMaxValidityMinutes: Int = Duration.ofHours(24).toMinutes().toInt() -private val discoveryDocumentMaxValiditySeconds: Int = Duration.ofHours(24).seconds.toInt() +private val discoveryDocumentMaxValidityMinutes: Int = 24.hours.inWholeMinutes.toInt() +private val discoveryDocumentMaxValiditySeconds: Int = 24.hours.inWholeSeconds.toInt() // // Flow with health card: @@ -128,7 +129,7 @@ class IdpBasicUseCase( val config = try { repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Instant.now()) + checkIdpConfigurationValidity(it, Clock.System.now()) } } catch (e: Exception) { Napier.e("IDP config couldn't be validated", e) @@ -136,7 +137,7 @@ class IdpBasicUseCase( // retry try { repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Instant.now()) + checkIdpConfigurationValidity(it, Clock.System.now()) } } catch (e: Exception) { Napier.e("IDP config couldn't be validated again; finally aborting", e) @@ -464,13 +465,13 @@ class IdpBasicUseCase( truststoreUseCase.checkIdpCertificate(config.certificate, true) val claims = JwtClaims().apply { - issuedAt = NumericDate.fromMilliseconds(config.issueTimestamp.toEpochMilli()) - expirationTime = NumericDate.fromMilliseconds(config.expirationTimestamp.toEpochMilli()) + issuedAt = NumericDate.fromMilliseconds(config.issueTimestamp.toEpochMilliseconds()) + expirationTime = NumericDate.fromMilliseconds(config.expirationTimestamp.toEpochMilliseconds()) } val r = NumericDateValidator().apply { setAllowedClockSkewSeconds(60) - setEvaluationTime(NumericDate.fromMilliseconds(timestamp.toEpochMilli())) + setEvaluationTime(NumericDate.fromMilliseconds(timestamp.toEpochMilliseconds())) setRequireExp(true) setRequireIat(true) setIatAllowedSecondsInThePast(discoveryDocumentMaxValiditySeconds) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt index 5a1f9bde..cd85c18e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt @@ -19,7 +19,7 @@ package de.gematik.ti.erp.app.prescription.model import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.time.Instant +import kotlinx.datetime.Instant object ScannedTaskData { data class ScannedTask( diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt index ee0b4802..64bd0fbe 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt @@ -19,11 +19,13 @@ package de.gematik.ti.erp.app.prescription.model import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 -import java.time.Duration -import java.time.Instant -import java.time.temporal.TemporalAccessor +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes -val CommunicationWaitStateDelta: Duration = Duration.ofMinutes(10) +val CommunicationWaitStateDelta: Duration = 10.minutes // gemSpec_FD_eRp: A_21267 Prozessparameter - Berechtigungen für Nutzer const val DIRECT_ASSIGNMENT_INDICATOR = "169" // direct assignment taskID starts with 169 @@ -37,10 +39,10 @@ object SyncedTaskData { ErxCommunicationDispReq, ErxCommunicationReply; fun toEntityValue() = when (this) { - CommunicationProfile.ErxCommunicationDispReq -> + ErxCommunicationDispReq -> CommunicationProfileV1.ErxCommunicationDispReq - CommunicationProfile.ErxCommunicationReply -> + ErxCommunicationReply -> CommunicationProfileV1.ErxCommunicationReply }.name } @@ -76,7 +78,7 @@ object SyncedTaskData { data class Other(val state: TaskStatus, val lastModified: Instant) : TaskState - fun state(now: Instant = Instant.now(), delta: Duration = CommunicationWaitStateDelta): TaskState = + fun state(now: Instant = Clock.System.now(), delta: Duration = CommunicationWaitStateDelta): TaskState = when { medicationRequest.multiplePrescriptionInfo.indicator && medicationRequest.multiplePrescriptionInfo.start?.let { start -> @@ -86,8 +88,8 @@ object SyncedTaskData { } expiresOn != null && expiresOn < now && status != TaskStatus.Completed -> Expired(expiresOn) - status == TaskStatus.Ready && - accessCode != null && + + status == TaskStatus.Ready && accessCode != null && communications.any { it.profile == CommunicationProfile.ErxCommunicationDispReq } && redeemState(now, delta) == RedeemState.NotRedeemable -> { val comm = this.communications @@ -128,7 +130,7 @@ object SyncedTaskData { * The list of redeemable prescriptions. Should NOT be used as a filter for the active/archive tab! * See [isActive] for a decision it this prescription should be shown in the "Active" or "Archive" tab. */ - fun redeemState(now: Instant = Instant.now(), delta: Duration = CommunicationWaitStateDelta): RedeemState { + fun redeemState(now: Instant = Clock.System.now(), delta: Duration = CommunicationWaitStateDelta): RedeemState { val expired = (expiresOn != null && expiresOn <= now) val redeemableLater = medicationRequest.multiplePrescriptionInfo.indicator && medicationRequest.multiplePrescriptionInfo.start?.let { @@ -139,7 +141,9 @@ object SyncedTaskData { val latestDispenseReqCommunication = communications .filter { it.profile == CommunicationProfile.ErxCommunicationDispReq } .maxOfOrNull { it.sentOn } - val isDeltaLocked = latestDispenseReqCommunication?.let { (it + delta) > now } + // if lastModified is more recent than the latest disp req, we can be sure that something + // happened with the task (e.g. claimed -> rejected) + val isDeltaLocked = latestDispenseReqCommunication?.let { lastModified < it && (it + delta) > now } return when { redeemableLater || expired -> RedeemState.NotRedeemable @@ -150,7 +154,7 @@ object SyncedTaskData { } } - fun isActive(now: Instant = Instant.now()): Boolean { + fun isActive(now: Instant = Clock.System.now()): Boolean { val notExpired = (expiresOn != null && now <= expiresOn) || expiresOn == null val allowedStatus = status == TaskStatus.Ready || status == TaskStatus.InProgress return notExpired && allowedStatus @@ -165,17 +169,8 @@ object SyncedTaskData { else -> true } - fun medicationRequestMedicationName() = - when (medicationRequest.medication) { - is MedicationPZN -> medicationRequest.medication.text - is MedicationCompounding -> medicationRequest.medication.text - is MedicationIngredient -> medicationRequest.medication.ingredients.firstOrNull()?.text ?: "" - is MedicationFreeText -> medicationRequest.medication.text - else -> "" - }.takeIf { it.isNotEmpty() } - - fun medicationName() = medicationRequestMedicationName() fun organizationName() = organization.name ?: practitioner.name + fun medicationName(): String? = medicationRequest.medication?.name() } data class Address( @@ -210,7 +205,7 @@ object SyncedTaskData { data class Patient( val name: String?, val address: Address?, - val birthdate: Instant?, + val birthdate: FhirTemporal?, val insuranceIdentifier: String? ) @@ -275,6 +270,7 @@ object SyncedTaskData { ARZNEI_UND_VERBAND_MITTEL, BTM, AMVV, + SONSTIGES, UNKNOWN; } @@ -297,12 +293,14 @@ object SyncedTaskData { ) sealed interface Medication { + fun name(): String + val category: MedicationCategory val vaccine: Boolean val text: String val form: String? val lotNumber: String? - val expirationDate: TemporalAccessor? + val expirationDate: FhirTemporal? } data class MedicationFreeText( @@ -311,8 +309,10 @@ object SyncedTaskData { override val text: String, override val form: String?, override val lotNumber: String?, - override val expirationDate: TemporalAccessor? - ) : Medication + override val expirationDate: FhirTemporal? + ) : Medication { + override fun name(): String = text + } data class MedicationIngredient( override val category: MedicationCategory, @@ -320,12 +320,14 @@ object SyncedTaskData { override val text: String, override val form: String?, override val lotNumber: String?, - override val expirationDate: TemporalAccessor?, + override val expirationDate: FhirTemporal?, val normSizeCode: String?, val amount: Ratio?, val ingredients: List - ) : Medication + ) : Medication { + override fun name(): String = joinIngredientNames(ingredients) + } data class MedicationCompounding( override val category: MedicationCategory, @@ -333,13 +335,15 @@ object SyncedTaskData { override val text: String, override val form: String?, override val lotNumber: String?, - override val expirationDate: TemporalAccessor?, + override val expirationDate: FhirTemporal?, val manufacturingInstructions: String?, val packaging: String?, val amount: Ratio?, val ingredients: List - ) : Medication + ) : Medication { + override fun name(): String = joinIngredientNames(ingredients) + } data class MedicationPZN( override val category: MedicationCategory, @@ -347,11 +351,14 @@ object SyncedTaskData { override val text: String, override val form: String?, override val lotNumber: String?, - override val expirationDate: TemporalAccessor?, + override val expirationDate: FhirTemporal?, val uniqueIdentifier: String, val normSizeCode: String?, val amount: Ratio? - ) : Medication + + ) : Medication { + override fun name() = text + } data class Communication( val taskId: String, @@ -364,4 +371,9 @@ object SyncedTaskData { val payload: String?, val consumed: Boolean ) + + fun joinIngredientNames(ingredients: List) = + ingredients.joinToString(", ") { ingredient -> + ingredient.text + } } 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 a72f8525..3d6974c5 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 @@ -51,6 +51,7 @@ import de.gematik.ti.erp.app.fhir.model.extractKBVBundle import de.gematik.ti.erp.app.fhir.model.extractMedicationDispense import de.gematik.ti.erp.app.fhir.model.extractTask import de.gematik.ti.erp.app.fhir.model.extractTaskAndKBVBundle +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 de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -64,10 +65,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import kotlinx.serialization.json.JsonElement -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset class TaskLocalDataSource( private val realm: Realm @@ -98,9 +99,10 @@ class TaskLocalDataSource( process = { taskResource, bundleResource -> extractTask( task = taskResource, - process = { taskId: String, accessCode: String?, lastModified: Instant, - expiresOn: LocalDate?, acceptUntil: LocalDate?, authoredOn: Instant, - status: TaskStatus -> + process = { taskId: String, accessCode: String?, lastModified: FhirTemporal.Instant, + expiresOn: FhirTemporal.LocalDate?, acceptUntil: FhirTemporal.LocalDate?, + authoredOn: FhirTemporal.Instant, status: TaskStatus -> + taskEntity = queryFirst("taskId = $0", taskId) ?: run { copyToRealm(SyncedTaskEntityV1()).also { profile.syncedTasks += it @@ -111,7 +113,7 @@ class TaskLocalDataSource( this.parent = profile this.taskId = taskId this.accessCode = accessCode - this.lastModified = lastModified.toRealmInstant() + this.lastModified = lastModified.value.toRealmInstant() this.status = when (status) { TaskStatus.Ready -> TaskStatusV1.Ready TaskStatus.InProgress -> TaskStatusV1.InProgress @@ -127,9 +129,10 @@ class TaskLocalDataSource( else -> TaskStatusV1.Other } this.expiresOn = - expiresOn?.atStartOfDay()?.toRealmInstant(ZoneOffset.UTC) - this.acceptUntil = acceptUntil?.atStartOfDay()?.toRealmInstant(ZoneOffset.UTC) - this.authoredOn = authoredOn.toRealmInstant() + expiresOn?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() + this.acceptUntil = + acceptUntil?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() + this.authoredOn = authoredOn.value.toRealmInstant() } } ) @@ -150,7 +153,7 @@ class TaskLocalDataSource( PatientEntityV1().apply { this.name = name this.address = address - this.birthdate = birthDate?.atStartOfDay()?.toRealmInstant() + this.dateOfBirth = birthDate this.insuranceIdentifier = insuranceIdentifier } }, @@ -223,6 +226,7 @@ class TaskLocalDataSource( MedicationCategory.BTM -> MedicationCategoryV1.BTM MedicationCategory.AMVV -> MedicationCategoryV1.AMVV + MedicationCategory.SONSTIGES -> MedicationCategoryV1.SONSTIGES else -> MedicationCategoryV1.UNKNOWN } this.form = form @@ -239,7 +243,7 @@ class TaskLocalDataSource( MultiplePrescriptionInfoEntityV1().apply { this.indicator = indicator this.numbering = numbering - this.start = start?.atStartOfDay()?.toRealmInstant() + this.start = start?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() } }, processMedicationRequest = { dateOfAccident, @@ -255,7 +259,8 @@ class TaskLocalDataSource( additionalFee -> MedicationRequestEntityV1().apply { - this.dateOfAccident = dateOfAccident?.atStartOfDay()?.toRealmInstant() + this.dateOfAccident = + dateOfAccident?.value?.atStartOfDayIn(TimeZone.UTC)?.toRealmInstant() this.location = location this.accidentType = when (accidentType) { AccidentType.Unfall -> AccidentTypeV1.Unfall @@ -398,7 +403,7 @@ class TaskLocalDataSource( this.wasSubstituted = wasSubstituted this.dosageInstruction = dosageInstruction this.performer = performer - this.whenHandedOver = whenHandedOver.atStartOfDay().toRealmInstant() + this.whenHandedOver = whenHandedOver.value.atStartOfDayIn(TimeZone.UTC).toRealmInstant() } } } @@ -444,7 +449,7 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = postalCodeAndCity = it.postalCodeAndCity ) }, - birthdate = this.patient?.birthdate?.toInstant(), + birthdate = this.patient?.dateOfBirth, insuranceIdentifier = this.patient?.insuranceIdentifier ), insuranceInformation = SyncedTaskData.InsuranceInformation( @@ -605,6 +610,7 @@ private fun MedicationCategoryV1?.toMedicationCategory(): SyncedTaskData.Medicat MedicationCategoryV1.ARZNEI_UND_VERBAND_MITTEL -> SyncedTaskData.MedicationCategory.ARZNEI_UND_VERBAND_MITTEL MedicationCategoryV1.BTM -> SyncedTaskData.MedicationCategory.BTM MedicationCategoryV1.AMVV -> SyncedTaskData.MedicationCategory.AMVV + MedicationCategoryV1.SONSTIGES -> SyncedTaskData.MedicationCategory.SONSTIGES else -> SyncedTaskData.MedicationCategory.UNKNOWN } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt index c662e07d..8df2c39d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.first import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext -import java.time.Instant +import kotlinx.datetime.Instant private const val TasksMaxPageSize = 50 @@ -44,6 +44,8 @@ class TaskRepository( it ?: 0 } + override val tag: String = "TaskRepository" + override suspend fun downloadResource( profileId: ProfileIdentifier, timestamp: String?, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt index 34e535cd..a606f6f6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt @@ -18,9 +18,10 @@ package de.gematik.ti.erp.app.profiles.model +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.time.Instant +import kotlinx.datetime.Instant object ProfilesData { enum class AvatarFigure { @@ -48,6 +49,7 @@ object ProfilesData { val insurantName: String? = null, val insuranceIdentifier: String? = null, val insuranceName: String? = null, + val insuranceType: InsuranceTypeV1, val lastAuthenticated: Instant? = null, val lastAuditEventSynced: Instant? = null, val lastTaskSynced: Instant? = null, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt index 983b5287..3d083647 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.profiles.repository import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.db.entities.deleteAll import de.gematik.ti.erp.app.db.entities.v1.AvatarFigureV1 +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileColorNamesV1 import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 import de.gematik.ti.erp.app.db.queryFirst @@ -34,7 +35,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.time.Instant +import kotlinx.datetime.Instant typealias ProfileIdentifier = String @@ -86,6 +87,7 @@ class ProfilesRepository constructor( insurantName = profile.insurantName ?: "", insuranceIdentifier = profile.insuranceIdentifier, insuranceName = profile.insuranceName, + insuranceType = profile.insuranceType, lastAuthenticated = profile.lastAuthenticated?.toInstant(), lastAuditEventSynced = profile.lastAuditEventSynced?.toInstant(), lastTaskSynced = profile.lastTaskSynced?.toInstant(), @@ -263,4 +265,12 @@ class ProfilesRepository constructor( } } } + + suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { + realm.write { + queryFirst("id = $0", profileId)?.apply { + this.insuranceType = InsuranceTypeV1.PKV + } + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt index a016f31e..edc9f4da 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/model/AuditEventData.kt @@ -18,7 +18,7 @@ package de.gematik.ti.erp.app.protocol.model -import java.time.Instant +import kotlinx.datetime.Instant object AuditEventData { data class AuditEvent( diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventLocalDataSource.kt index 605db78f..369eedc2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventLocalDataSource.kt @@ -63,7 +63,7 @@ class AuditEventLocalDataSource( AuditEventEntityV1().apply { this.id = id this.text = description - this.timestamp = timestamp.toRealmInstant() + this.timestamp = timestamp.value.toRealmInstant() this.taskId = taskId this.profile = profile } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt index ffe3c272..90600913 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt @@ -24,7 +24,7 @@ import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext -import java.time.Instant +import kotlinx.datetime.Instant private const val AuditEventsMaxPageSize = 50 @@ -33,6 +33,7 @@ class AuditEventsRepository( private val localDataSource: AuditEventLocalDataSource, private val dispatchers: DispatchProvider ) : ResourcePaging(dispatchers, AuditEventsMaxPageSize) { + override val tag: String = "AuditEventsRepository" suspend fun downloadAuditEvents(profileId: ProfileIdentifier) = downloadPaged(profileId) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt index 9c58c6bd..f2351dc0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/GeneralSettings.kt @@ -20,16 +20,17 @@ package de.gematik.ti.erp.app.settings import de.gematik.ti.erp.app.settings.model.SettingsData import kotlinx.coroutines.flow.Flow -import java.time.Instant +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant interface GeneralSettings { val general: Flow - suspend fun acceptUpdatedDataTerms(now: Instant = Instant.now()) + suspend fun acceptUpdatedDataTerms(now: Instant = Clock.System.now()) suspend fun saveOnboardingSucceededData( authenticationMode: SettingsData.AuthenticationMode, profileName: String, - now: Instant = Instant.now() + now: Instant = Clock.System.now() ) suspend fun saveAuthenticationMode(mode: SettingsData.AuthenticationMode) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt index 70921734..4ce1c0e1 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/model/SettingsData.kt @@ -20,7 +20,6 @@ package de.gematik.ti.erp.app.settings.model import de.gematik.ti.erp.app.secureRandomInstance import java.security.MessageDigest -import java.time.Instant object SettingsData { data class General( @@ -28,7 +27,6 @@ object SettingsData { val onboardingShownIn: AppVersion?, val welcomeDrawerShown: Boolean, val mainScreenTooltipsShown: Boolean, - val dataProtectionVersionAcceptedOn: Instant, val zoomEnabled: Boolean, val userHasAcceptedInsecureDevice: Boolean, val authenticationFails: Int, 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 eb505013..244a082b 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 @@ -22,7 +22,6 @@ import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.db.entities.v1.ProfileEntityV1 import de.gematik.ti.erp.app.db.entities.v1.SettingsAuthenticationMethodV1 import de.gematik.ti.erp.app.db.entities.v1.SettingsEntityV1 -import de.gematik.ti.erp.app.db.toInstant import de.gematik.ti.erp.app.db.toRealmInstant import de.gematik.ti.erp.app.db.writeToRealm import de.gematik.ti.erp.app.settings.GeneralSettings @@ -30,12 +29,12 @@ import de.gematik.ti.erp.app.settings.PharmacySettings import de.gematik.ti.erp.app.settings.model.SettingsData import io.realm.kotlin.Realm import io.realm.kotlin.ext.query -import java.time.Instant import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant @Suppress("TooManyFunctions") class SettingsRepository constructor( @@ -62,7 +61,6 @@ class SettingsRepository constructor( null }, welcomeDrawerShown = it.welcomeDrawerShown, - dataProtectionVersionAcceptedOn = it.dataProtectionVersionAccepted.toInstant(), zoomEnabled = it.zoomEnabled, userHasAcceptedInsecureDevice = it.userHasAcceptedInsecureDevice, authenticationFails = it.authenticationFails, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt index 8e27fc70..a7c57e2c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/CertUtils.kt @@ -18,6 +18,9 @@ package de.gematik.ti.erp.app.vau +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import kotlinx.datetime.toKotlinInstant import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.ocsp.BasicOCSPResp @@ -25,7 +28,6 @@ import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi import org.bouncycastle.jce.interfaces.ECPublicKey import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder import org.bouncycastle.operator.bc.BcECContentVerifierProviderBuilder -import java.time.Instant import java.util.Date /** @@ -42,10 +44,10 @@ fun List>.filterByOIDAndOCSPResponse( filter { it.first().containsIdentifierOid(oid) } .filter { chain -> validOcspResponses.find { validOcspResponse -> - val producedAt = validOcspResponse.producedAt.toInstant() + val producedAt = validOcspResponse.producedAt.toInstant().toKotlinInstant() validOcspResponse.findValidCert(chain.first().serialNumber)?.let { - val thisUpdate = it.thisUpdate.toInstant() + val thisUpdate = it.thisUpdate.toInstant().toKotlinInstant() (producedAt <= timestamp) && (thisUpdate <= timestamp) && it.matchesIssuer(chain[1]) @@ -98,7 +100,7 @@ internal fun X509CertificateHolder.subjectDNContainsCNPrefixWithNumber(cnPrefix: * Throws an exception if the check fails. */ fun X509CertificateHolder.checkValidity(timestamp: Instant) { - require(isValidOn(Date.from(timestamp))) + require(isValidOn(Date.from(timestamp.toJavaInstant()))) } fun X509CertificateHolder.extractECPublicKey(): ECPublicKey { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt index 56ef659b..2155d807 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/OCSPUtils.kt @@ -18,6 +18,8 @@ package de.gematik.ti.erp.app.vau +import kotlinx.datetime.Instant +import kotlinx.datetime.toKotlinInstant import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.isismtt.ocsp.CertHash import org.bouncycastle.cert.X509CertificateHolder @@ -27,8 +29,7 @@ import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder import org.bouncycastle.operator.bc.BcDigestCalculatorProvider import org.bouncycastle.operator.bc.BcECContentVerifierProviderBuilder import java.math.BigInteger -import java.time.Duration -import java.time.Instant +import kotlin.time.Duration private val certHashOid = ASN1ObjectIdentifier("1.3.36.8.3.13") @@ -85,6 +86,6 @@ fun BasicOCSPResp.checkSignatureWith(signatureCertificate: X509CertificateHolder */ fun BasicOCSPResp.checkValidity(maxAge: Duration, timestamp: Instant) { requireNotNull( - this.producedAt?.toInstant()?.takeIf { it + maxAge >= timestamp && timestamp >= it } + this.producedAt?.toInstant()?.toKotlinInstant()?.takeIf { it + maxAge >= timestamp && timestamp >= it } ) { "OCSP response expired" } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt index 9ef51dec..48ab5940 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreConfig.kt @@ -21,11 +21,12 @@ package de.gematik.ti.erp.app.vau.usecase import de.gematik.ti.erp.app.BuildKonfig import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.util.encoders.Base64 -import java.time.Duration +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours class TruststoreConfig(getTrustAnchor: () -> String) { val maxOCSPResponseAge: Duration by lazy { - Duration.ofHours(BuildKonfig.VAU_OCSP_RESPONSE_MAX_AGE) + BuildKonfig.VAU_OCSP_RESPONSE_MAX_AGE.hours } val trustAnchor by lazy { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt index c41d430c..9c01c692 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/usecase/TruststoreUseCase.kt @@ -32,10 +32,10 @@ import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.ocsp.BasicOCSPResp import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi import io.github.aakira.napier.Napier +import kotlinx.datetime.Instant import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey -import java.time.Duration -import java.time.Instant +import kotlin.time.Duration private const val RCA_PREFIX = "GEM.RCA" private const val CA_PREFIX = "GEM.KOMP-CA" diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt index 3ee03884..be8e699a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/api/ResourcePagingTest.kt @@ -23,8 +23,8 @@ import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import org.junit.Rule -import java.time.Instant import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -43,7 +43,7 @@ private class PageTestContainer(dispatchers: DispatchProvider) : ResourcePaging< return Result.success(ResourceResult(10, Unit)) } - override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant = Instant.parse("2022-03-22T12:30:00.00Z") } @@ -62,7 +62,7 @@ private class PageTestContainerWithError(dispatchers: DispatchProvider) : return Result.failure(IllegalArgumentException()) } - override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant = Instant.parse("2022-03-22T12:30:00.00Z") } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt index 253b6672..4660bec1 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/RealmInstantConverterTest.kt @@ -19,24 +19,27 @@ package de.gematik.ti.erp.app.db import io.realm.kotlin.types.RealmInstant -import java.time.Instant -import java.time.LocalDateTime -import java.time.OffsetDateTime -import java.time.ZoneOffset +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.asTimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime import kotlin.test.Test import kotlin.test.assertEquals class RealmInstantConverterTest { @Test fun `RealmInstant to LocalDateTime`() { - val dt = RealmInstant.from(123456, 123456789).toLocalDateTime() - assertEquals(123456, dt.toEpochSecond(ZoneOffset.UTC)) - assertEquals(123456789, dt.nano) + val dt = RealmInstant.from(123456, 123456789).toLocalDateTime().toInstant(TimeZone.UTC) + assertEquals(123456, dt.epochSeconds) + assertEquals(123456789, dt.nanosecondsOfSecond) } @Test fun `LocalDateTime to RealmInstant`() { - val ri = LocalDateTime.ofEpochSecond(123456, 123456789, ZoneOffset.UTC).toRealmInstant() + val ri = Instant.fromEpochSeconds(123456, 123456789).toRealmInstant() assertEquals(123456, ri.epochSeconds) assertEquals(123456789, ri.nanosecondsOfSecond) } @@ -44,29 +47,28 @@ class RealmInstantConverterTest { @Test fun `RealmInstant to Instant`() { val dt = RealmInstant.from(123456, 123456789).toInstant() - assertEquals(123456, dt.epochSecond) - assertEquals(123456789, dt.nano) + assertEquals(123456, dt.epochSeconds) + assertEquals(123456789, dt.nanosecondsOfSecond) } @Test fun `Instant to RealmInstant`() { - val ri = Instant.ofEpochSecond(123456, 123456789).toRealmInstant() + val ri = Instant.fromEpochSeconds(123456, 123456789).toRealmInstant() assertEquals(123456, ri.epochSeconds) assertEquals(123456789, ri.nanosecondsOfSecond) } @Test fun `Convert with offset`() { - val dtPlus2 = OffsetDateTime.parse("2022-02-04T14:05:10+02:00") - val dtUTC = OffsetDateTime.parse("2022-02-04T12:05:10+00:00") - val timestampAtUTC = dtPlus2.toEpochSecond() + val dtPlus2 = Instant.parse("2022-02-04T14:05:10+02:00") + val timestampAtUTC = dtPlus2.epochSeconds - assertEquals(dtUTC, dtPlus2.withOffsetSameInstant(ZoneOffset.UTC)) - - val realmInstantAtUTC = dtPlus2.toLocalDateTime().toRealmInstant(ZoneOffset.ofHours(2)) + val realmInstantAtUTC = dtPlus2.toLocalDateTime(UtcOffset(hours = 2).asTimeZone()).toRealmInstant( + UtcOffset(hours = 2).asTimeZone() + ) assertEquals(timestampAtUTC, realmInstantAtUTC.epochSeconds) - val localDateTimeAtPlus2 = realmInstantAtUTC.toLocalDateTime(ZoneOffset.ofHours(2)) + val localDateTimeAtPlus2 = realmInstantAtUTC.toLocalDateTime(UtcOffset(hours = 2).asTimeZone()) assertEquals(LocalDateTime.parse("2022-02-04T14:05:10"), localDateTimeAtPlus2) } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt index 57173bee..3ae18757 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/db/entities/DelegatesTest.kt @@ -18,15 +18,16 @@ package de.gematik.ti.erp.app.db.entities +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.Instant import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFails import org.bouncycastle.util.encoders.Base64 -import java.time.Instant -import java.time.Year -import java.time.temporal.TemporalAccessor private object Clazz { enum class EnumA { @@ -151,14 +152,14 @@ class DelegatesTest { fun `date time parsing`() { val container = object { var backingProp: String? = "2015-02-07T13:28:17.243+00:00" - var prop: TemporalAccessor? by temporalAccessorNullable(::backingProp) + var prop: FhirTemporal? by temporalAccessorNullable(::backingProp) } - assertEquals(Instant.parse("2015-02-07T13:28:17.243+00:00"), container.prop) + assertEquals(Instant.parse("2015-02-07T13:28:17.243+00:00"), (container.prop as FhirTemporal.Instant).value) - container.prop = Year.parse("2023") + container.prop = Year.parse("2023").asFhirTemporal() assertEquals("2023", container.backingProp) - assertEquals(Year.parse("2023"), container.prop) + assertEquals(Year.parse("2023"), (container.prop as FhirTemporal.Year).value) } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapperTest.kt index e7b9b0ba..8b63829a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/AuditEventMapperTest.kt @@ -20,13 +20,19 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import java.io.File -import java.time.Instant import kotlin.test.Test import kotlin.test.assertEquals -private val testBundle by lazy { File("$ResourceBasePath/audit_events_bundle.json").readText() } +private val testBundle by lazy { + File("$ResourceBasePath/audit_events_bundle.json").readText() +} +private val testAuditEventVersion12 by lazy { + File("$ResourceBasePath/audit_events_bundle_version_1_2.json").readText() +} class AuditEventMapperTest { private class AuditEvent( @@ -58,6 +64,15 @@ class AuditEventMapperTest { ) ) + private val auditEventsVersion12 = mapOf( + 0 to AuditEvent( + id = "9361863d-fec0-4ba9-8776-7905cf1b0cfa", + taskId = null, + description = "Praxis Dr. Müller, Bahnhofstr. 78 hat ein E-Rezept 160.123.456.789.123.58 eingestellt", + timestamp = Instant.parse("2022-04-27T08:04:27.434Z") + ) + ) + @Test fun `parse audit events`() { var index = 0 @@ -69,7 +84,7 @@ class AuditEventMapperTest { assertEquals(ev.id, id) assertEquals(ev.taskId, taskId) assertEquals(ev.description, description) - assertEquals(ev.timestamp, timestamp) + assertEquals(ev.timestamp, timestamp.value) } index++ @@ -77,4 +92,18 @@ class AuditEventMapperTest { assertEquals(50, index) } + + @Test + fun `parse audit events version 1_2`() { + extractAuditEvents( + Json.parseToJsonElement(testAuditEventVersion12) + ) { id, taskId, description, timestamp -> + auditEventsVersion12[0]?.let { ev -> + assertEquals(ev.id, id) + assertEquals(ev.taskId, taskId) + assertEquals(ev.description, description) + assertEquals(ev.timestamp.asFhirTemporal(), timestamp) + } + } + } } 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 e25cdb2d..3de9544d 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,8 +18,8 @@ package de.gematik.ti.erp.app.fhir.model +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json -import java.time.LocalDate import kotlin.test.Test import kotlin.test.assertEquals @@ -173,7 +173,7 @@ class CommonRessourceMapperTest { processMultiplePrescriptionInfo = { indicator, numbering, start -> assertEquals(true, indicator) assertEquals(ReturnType.Ratio, numbering) - assertEquals(LocalDate.parse("2022-08-17"), start) + assertEquals(LocalDate.parse("2022-08-17"), start?.value) ReturnType.MultiplePrescriptionInfo } ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt index 5e328b48..515a711a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt @@ -20,11 +20,13 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal import de.gematik.ti.erp.app.fhir.parser.contained import de.gematik.ti.erp.app.fhir.parser.containedString +import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import java.io.File -import java.time.Instant import kotlin.test.Test import kotlin.test.assertEquals @@ -32,6 +34,7 @@ private const val JsonSymbols = "\"{}[]:" private const val JsonSymbolsEscaped = "\\\"{}[]:" private val testBundle by lazy { File("$ResourceBasePath/communications_bundle.json").readText() } +private val testBundleVersion12 by lazy { File("$ResourceBasePath/communications_bundle_version_1_2.json").readText() } class CommunicationMapperTest { @Test @@ -117,6 +120,19 @@ class CommunicationMapperTest { ) ) + private val communicationsVersion12 = mapOf( + 0 to Communication( + taskId = "160.000.033.491.280.78", + communicationId = "7977a4ab-97a9-4d95-afb3-6c4c1e2ac596", + orderId = null, + profile = CommunicationProfile.ErxCommunicationReply, + sentOn = Instant.parse("2020-04-29T11:46:30.128Z"), + sender = "3-SMC-B-Testkarte-883110000123465", + recipient = "X234567890", + payload = "Eisern" + ) + ) + @Test fun `parse communications`() { var index = 0 @@ -129,7 +145,7 @@ class CommunicationMapperTest { assertEquals(com.communicationId, communicationId) assertEquals(com.orderId, orderId) assertEquals(com.profile, profile) - assertEquals(com.sentOn, sentOn) + assertEquals(FhirTemporal.Instant(com.sentOn), sentOn) assertEquals(com.sender, sender) assertEquals(com.recipient, recipient) assertEquals(com.payload, payload) @@ -140,4 +156,22 @@ class CommunicationMapperTest { assertEquals(15, index) } + + @Test + fun `parse communications version 1_2`() { + extractCommunications( + Json.parseToJsonElement(testBundleVersion12) + ) { taskId, communicationId, orderId, profile, sentOn, sender, recipient, payload -> + communicationsVersion12[0]?.let { com -> + assertEquals(com.taskId, taskId) + assertEquals(com.communicationId, communicationId) + assertEquals(com.orderId, orderId) + assertEquals(com.profile, profile) + assertEquals(com.sentOn.asFhirTemporal(), sentOn) + assertEquals(com.sender, sender) + assertEquals(com.recipient, recipient) + assertEquals(com.payload, payload) + } + } + } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt index 670e595f..10d15bb3 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt @@ -18,10 +18,13 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json import org.junit.Test -import java.time.Instant -import java.time.LocalDate import kotlin.test.assertEquals class MedicationDispenseMapperTest { @@ -62,7 +65,7 @@ class MedicationDispenseMapperTest { assertEquals("06491772", uniqueIdentifier) assertEquals(listOf(), ingredients) assertEquals("8521037577", lotNumber) - assertEquals(Instant.parse("2023-05-02T06:26:06Z"), expirationDate) + assertEquals(FhirTemporal.Instant(Instant.parse("2023-05-02T06:26:06Z")), expirationDate) ReturnType.Medication }, processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, @@ -73,7 +76,7 @@ class MedicationDispenseMapperTest { assertEquals(false, wasSubstituted) assertEquals(null, dosageInstruction) assertEquals("3-SMC-B-Testkarte-883110000116873", performer) - assertEquals(LocalDate.parse("2022-07-12"), whenHandedOver) + assertEquals(FhirTemporal.LocalDate(LocalDate.parse("2022-07-12")), whenHandedOver) ReturnType.MedicationDispense } ) @@ -122,7 +125,112 @@ class MedicationDispenseMapperTest { assertEquals(false, wasSubstituted) assertEquals(null, dosageInstruction) assertEquals("3-SMC-B-Testkarte-883110000116873", performer) - assertEquals(LocalDate.parse("2022-07-12"), whenHandedOver) + assertEquals(LocalDate.parse("2022-07-12"), whenHandedOver.value) + ReturnType.MedicationDispense + } + ) + assertEquals(ReturnType.MedicationDispense, result) + } + + @Test + fun `extract medication dispenses version 1_2`() { + val medicationDispensesBundle = Json.parseToJsonElement(medDispenseBundleVersion_1_2) + medicationDispensesBundle.findAll("entry.resource").apply { + val result = extractMedicationDispense( + this.elementAt(0), + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { _, _, _, _, _ -> + ReturnType.Ingredient + }, + processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + ingredients, lotNumber, expirationDate -> + assertEquals("Sumatriptan-1a Pharma 100 mg Tabletten", text) + assertEquals(MedicationProfile.PZN, medicationProfile) + assertEquals(MedicationCategory.ARZNEI_UND_VERBAND_MITTEL, medicationCategory) + assertEquals("TAB", form) + assertEquals(ReturnType.Ratio, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals("N1", normSizeCode) + assertEquals("06313728", uniqueIdentifier) + assertEquals(listOf(), ingredients) + assertEquals(null, lotNumber) + assertEquals(null, expirationDate) + ReturnType.Medication + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, + dosageInstruction, performer, whenHandedOver -> + assertEquals("3465270a-11e7-4bbf-ae53-378f9cc52747", dispenseId) + assertEquals("X234567890", patientIdentifier) + assertEquals(ReturnType.Medication, medication) + assertEquals(false, wasSubstituted) + assertEquals("1-0-1-0", dosageInstruction) + assertEquals("3-abc-1234567890", performer) + assertEquals(LocalDate.parse("2022-02-28").asFhirTemporal(), whenHandedOver) + ReturnType.MedicationDispense + } + ) + assertEquals(ReturnType.MedicationDispense, result) + } + } + + @Test + fun `extract medication dispense with unknown medication category`() { + val medicationDispenseJson = Json.parseToJsonElement(medicationDispenseWithoutCategoryJson) + val result = extractMedicationDispense( + medicationDispenseJson, + quantityFn = { _, _ -> + ReturnType.Quantity + }, + ratioFn = { numerator, denominator -> + assertEquals(ReturnType.Quantity, numerator) + assertEquals(ReturnType.Quantity, denominator) + ReturnType.Ratio + }, + ingredientFn = { text, form, number, amount, strength -> + assertEquals("Wirkstoff Paulaner Weissbier", text) + assertEquals(null, form) + assertEquals("", number) + assertEquals(null, amount) + assertEquals(ReturnType.Ratio, strength) + ReturnType.Ingredient + }, + processMedication = { text, medicationProfile, medicationCategory, form, amount, vaccine, + manufacturingInstructions, packaging, normSizeCode, uniqueIdentifier, + ingredients, lotNumber, expirationDate -> + assertEquals("Defamipin", text) + assertEquals(MedicationProfile.PZN, medicationProfile) + assertEquals(MedicationCategory.UNKNOWN, medicationCategory) + assertEquals("FET", form) + assertEquals(ReturnType.Ratio, amount) + assertEquals(false, vaccine) + assertEquals(null, manufacturingInstructions) + assertEquals(null, packaging) + assertEquals("Sonstiges", normSizeCode) + assertEquals("06491772", uniqueIdentifier) + assertEquals(listOf(), ingredients) + assertEquals("8521037577", lotNumber) + assertEquals(Instant.parse("2023-05-02T06:26:06Z").asFhirTemporal(), expirationDate) + ReturnType.Medication + }, + processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, + dosageInstruction, performer, whenHandedOver -> + assertEquals("160.000.000.031.686.59", dispenseId) + assertEquals("X110535541", patientIdentifier) + assertEquals(ReturnType.Medication, medication) + assertEquals(false, wasSubstituted) + assertEquals(null, dosageInstruction) + assertEquals("3-SMC-B-Testkarte-883110000116873", performer) + assertEquals(LocalDate.parse("2022-07-12").asFhirTemporal(), whenHandedOver) ReturnType.MedicationDispense } ) 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 b0f8a4be..104ae383 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 @@ -18,10 +18,10 @@ package de.gematik.ti.erp.app.fhir.model +import kotlinx.datetime.LocalTime import kotlinx.serialization.json.Json import java.io.File import java.time.DayOfWeek -import java.time.LocalTime import kotlin.test.Test import kotlin.test.assertEquals diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt index 32c6a1b2..ba138501 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt @@ -18,8 +18,11 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.Year +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json -import java.time.LocalDate import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -45,7 +48,32 @@ class RessourceMapperVersion102Test { processPatient = { name, address, birthDate, insuranceIdentifier -> assertEquals("Prinzessin Lars Graf Freiherr von Schinder", name) assertEquals(ReturnType.Address, address) - assertEquals(LocalDate.parse("1964-04-04"), birthDate) + assertEquals(FhirTemporal.LocalDate(LocalDate.parse("1964-04-04")), birthDate) + assertEquals("X110535541", insuranceIdentifier) + + ReturnType.Patient + } + ) + + assertEquals(ReturnType.Patient, result) + } + + @Test + fun `process patient version 1_0_2 with incomplete date of birth`() { + val patient = Json.parseToJsonElement(patientJson_vers_1_0_2_with_incomplete_birthDate) + val result = extractPatient( + patient, + processAddress = { line, postalCode, city -> + assertEquals(listOf("Siegburger Str. 155"), line) + assertEquals("51105", postalCode) + assertEquals("Köln", city) + + ReturnType.Address + }, + processPatient = { name, address, birthDate, insuranceIdentifier -> + assertEquals("Prinzessin Lars Graf Freiherr von Schinder", name) + assertEquals(ReturnType.Address, address) + assertEquals(Year.parse("1964").asFhirTemporal(), birthDate) assertEquals("X110535541", insuranceIdentifier) ReturnType.Patient @@ -226,7 +254,7 @@ class RessourceMapperVersion102Test { processMultiplePrescriptionInfo = { indicator, numbering, start -> assertTrue(indicator) assertEquals(ReturnType.Ratio, numbering) - assertEquals(LocalDate.parse("2022-08-17"), start) + assertEquals(FhirTemporal.LocalDate(LocalDate.parse("2022-08-17")), start) ReturnType.MultiplePrescriptionInfo }, processMedicationRequest = { dateOfAccident, @@ -240,7 +268,7 @@ class RessourceMapperVersion102Test { note, bvg, additionalFee -> - assertEquals(LocalDate.parse("2022-06-29"), dateOfAccident) + assertEquals(FhirTemporal.LocalDate(LocalDate.parse("2022-06-29")), dateOfAccident) assertEquals("Dummy-Betrieb", location) assertEquals(AccidentType.Arbeitsunfall, accidentType) assertEquals(false, emergencyFee) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt index 7ad6c5de..69b07ea9 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion110Test.kt @@ -18,8 +18,9 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json -import java.time.LocalDate import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -41,7 +42,7 @@ class RessourceMapperVersion110Test { processPatient = { name, address, birthDate, insuranceIdentifier -> assertEquals("Ludger Königsstein", name) assertEquals(ReturnType.Address, address) - assertEquals(LocalDate.parse("1935-06-22"), birthDate) + assertEquals(LocalDate.parse("1935-06-22").asFhirTemporal(), birthDate) assertEquals("K220635158", insuranceIdentifier) ReturnType.Patient @@ -222,7 +223,7 @@ class RessourceMapperVersion110Test { processMultiplePrescriptionInfo = { indicator, numbering, start -> assertTrue(indicator) assertEquals(ReturnType.Ratio, numbering) - assertEquals(LocalDate.parse("2022-05-20"), start) + assertEquals(LocalDate.parse("2022-05-20").asFhirTemporal(), start) ReturnType.MultiplePrescriptionInfo }, processMedicationRequest = { dateOfAccident, diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt index b36880f3..42b3f83e 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt @@ -18,10 +18,11 @@ package de.gematik.ti.erp.app.fhir.model +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json import org.junit.Test -import java.time.Instant -import java.time.LocalDate import kotlin.test.assertEquals class TaskMapperTest { @@ -34,10 +35,10 @@ class TaskMapperTest { process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> assertEquals("160.000.000.029.982.30", taskId) assertEquals("dd23212d35d14ccde351f9a1077f3d9508dcb8629882627ec16a22ea86144290", accessCode) - assertEquals(Instant.parse("2022-06-09T11:57:37.923Z"), lastModified) - assertEquals(LocalDate.parse("2022-09-09"), expiresOn) - assertEquals(LocalDate.parse("2022-07-07"), acceptUntil) - assertEquals(Instant.parse("2022-06-09T11:50:23.223Z"), authoredOn) + assertEquals(Instant.parse("2022-06-09T11:57:37.923Z").asFhirTemporal(), lastModified) + assertEquals(LocalDate.parse("2022-09-09").asFhirTemporal(), expiresOn) + assertEquals(LocalDate.parse("2022-07-07").asFhirTemporal(), acceptUntil) + assertEquals(Instant.parse("2022-06-09T11:50:23.223Z").asFhirTemporal(), authoredOn) assertEquals(TaskStatus.Completed, status) } ) @@ -51,12 +52,20 @@ class TaskMapperTest { process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> assertEquals("160.000.033.491.280.78", taskId) assertEquals("777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea", accessCode) - assertEquals(Instant.parse("2022-03-18T15:27:00Z"), lastModified) - assertEquals(LocalDate.parse("2022-06-02"), expiresOn) - assertEquals(LocalDate.parse("2022-04-02"), acceptUntil) - assertEquals(Instant.parse("2022-03-18T15:26:00Z"), authoredOn) - assertEquals(TaskStatus.Ready, status) + assertEquals(Instant.parse("2022-03-18T15:29:00Z").asFhirTemporal(), lastModified) + assertEquals(LocalDate.parse("2022-06-02").asFhirTemporal(), expiresOn) + assertEquals(LocalDate.parse("2022-04-02").asFhirTemporal(), acceptUntil) + assertEquals(Instant.parse("2022-03-18T15:26:00Z").asFhirTemporal(), authoredOn) + assertEquals(TaskStatus.Completed, status) } ) } + + @Test + fun `extract task id from bundle`() { + val taskJson = Json.parseToJsonElement(task_bundle_version_1_2) + val (bundleTotal, taskIds) = extractTaskIds(taskJson) + assertEquals(1, bundleTotal) + assertEquals("160.000.033.491.280.78", taskIds[0]) + } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt index 0e9180f8..43ada2db 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt @@ -36,6 +36,11 @@ val organizationJson by lazy { val patientJson_vers_1_0_2 by lazy { File("$ResourceBasePath/fhir/patient.json").readText() } + +val patientJson_vers_1_0_2_with_incomplete_birthDate by lazy { + File("$ResourceBasePath/fhir/patient_incomplete_birth_date.json").readText() +} + val patientJson_vers_1_1_0 by lazy { File("$ResourceBasePath/fhir/patient_vers_1_1_0.json").readText() } @@ -107,6 +112,18 @@ val medicationDispenseJson by lazy { File("$ResourceBasePath/fhir/medication_dispense.json").readText() } +val medicationDispenseWithoutCategoryJson by lazy { + File("$ResourceBasePath/fhir/medication_dispense_without_category.json").readText() +} + val medicationDispenseWithUnknownMedicationProfileJson by lazy { File("$ResourceBasePath/fhir/medication_dispense_unknown_medication_profile.json").readText() } + +val medDispenseBundleVersion_1_2 by lazy { + File("$ResourceBasePath/fhir/bundle_med_dispense_version_1_2.json").readText() +} + +val task_bundle_version_1_2 by lazy { + File("$ResourceBasePath/fhir/task_bundle_vers_1_2.json").readText() +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt index 989ff518..8fab322e 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/ConverterTest.kt @@ -19,79 +19,13 @@ package de.gematik.ti.erp.app.fhir.parser import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject import kotlin.test.Test -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.Year -import java.time.YearMonth import kotlin.test.assertEquals import kotlin.test.assertFails -import kotlin.test.assertIs import kotlin.test.assertNull class ConverterTest { - private val fhirInstant = listOf( - "2015-02-07T13:28:17+02:00", - "2015-02-07T13:28:17+00:00", - "2015-02-07T13:28:17.243+00:00", - "2022-01-13T15:44:15.816+00:00" - ) - - private val fhirLocalDate = listOf( - "2015-02-03", - "2011-03-12" - ) - - private val fhirYearMonth = listOf( - "2015-02", - "1999-01" - ) - - private val fhirYear = listOf( - "2015", - "1999" - ) - - private val fhirTime = listOf( - "13:28:00", - "13:28:17" - ) - - @Test - fun `convert dates expecting type`() { - fhirInstant.forEach { - assertIs(JsonPrimitive(it).asTemporalAccessor()) - } - fhirLocalDate.forEach { - assertIs(JsonPrimitive(it).asTemporalAccessor()) - } - fhirYearMonth.forEach { - assertIs(JsonPrimitive(it).asTemporalAccessor()) - } - fhirYear.forEach { - assertIs(JsonPrimitive(it).asTemporalAccessor()) - } - fhirTime.forEach { - assertIs(JsonPrimitive(it).asTemporalAccessor()) - } - } - - @Test - fun `convert dates to string`() { - assertEquals("2022-01-13T15:44:15.816Z", Instant.parse("2022-01-13T15:44:15.816+00:00").asFormattedString()) - assertEquals("2015-02-07T11:28:17Z", Instant.parse("2015-02-07T13:28:17+02:00").asFormattedString()) - assertEquals("2015-02-07T11:28:17", LocalDateTime.parse("2015-02-07T11:28:17").asFormattedString()) - assertEquals("2015-02-03", LocalDate.parse("2015-02-03").asFormattedString()) - assertEquals("2015-02", YearMonth.parse("2015-02").asFormattedString()) - assertEquals("2015", Year.parse("2015").asFormattedString()) - assertEquals("13:28:00", LocalTime.parse("13:28:00").asFormattedString()) - - assertEquals(Instant.parse("2022-01-13T15:44:15.816+00:00"), "2022-01-13T15:44:15.816Z".asTemporalAccessor()) - } @Test fun `contained primitive - string`() { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt new file mode 100644 index 00000000..cd3f0ab6 --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/parser/TemporalConverterTest.kt @@ -0,0 +1,51 @@ +/* + * 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.fhir.parser + +import kotlin.test.Test +import kotlin.test.assertEquals + +class TemporalConverterTest { + + @Test + fun `convert dates to string`() { + assertEquals("2022-01-13T15:44:15.816Z", "2022-01-13T15:44:15.816+00:00".toFhirTemporal().formattedString()) + assertEquals("2022-01-13T15:44:15.816Z", "2022-01-13T15:44:15.816Z".toFhirTemporal().formattedString()) + assertEquals("2022-01-13T15:44:00Z", "2022-01-13T15:44:00+00:00".toFhirTemporal().formattedString()) + assertEquals("2015-02-07T11:28:17Z", "2015-02-07T13:28:17+02:00".toFhirTemporal().formattedString()) + assertEquals("2015-02-07T15:28:17Z", "2015-02-07T13:28:17-02:00".toFhirTemporal().formattedString()) + + assertEquals("2015-02-07T11:28:17", "2015-02-07T11:28:17".toFhirTemporal().formattedString()) + assertEquals("2015-02-07T11:28", "2015-02-07T11:28:00".toFhirTemporal().formattedString()) + assertEquals("2015-02-07T11:28:00.123", "2015-02-07T11:28:00.123".toFhirTemporal().formattedString()) + + assertEquals("2015-02-03", "2015-02-03".toFhirTemporal().formattedString()) + assertEquals("2015-02", "2015-02".toFhirTemporal().formattedString()) + assertEquals("2015", "2015".toFhirTemporal().formattedString()) + + assertEquals("13:28:05", "13:28:05".toFhirTemporal().formattedString()) + assertEquals("13:00", "13:00:00".toFhirTemporal().formattedString()) + assertEquals("13:28", "13:28:00".toFhirTemporal().formattedString()) + + assertEquals( + kotlinx.datetime.Instant.parse("2022-01-13T15:44:15.816+00:00"), + ("2022-01-13T15:44:15.816Z".toFhirTemporal() as FhirTemporal.Instant).value + ) + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt index 457e7b0b..d3476a19 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt @@ -58,6 +58,7 @@ import io.realm.kotlin.RealmConfiguration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder import org.jose4j.base64url.Base64 import org.jose4j.jws.JsonWebSignature @@ -65,7 +66,6 @@ import org.jose4j.jwx.JsonWebStructure import org.junit.Rule import java.io.File import java.security.Security -import java.time.Instant import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -111,8 +111,8 @@ class CommonIdpRepositoryTest : TestDB() { pukIdpEncEndpoint = "http://localhost:8888/idpEnc/jwk.json", pukIdpSigEndpoint = "http://localhost:8888/ipdSig/jwk.json", certificate = x509Certificate, - expirationTimestamp = Instant.ofEpochSecond(EXPECTED_EXPIRATION_TIME), - issueTimestamp = Instant.ofEpochSecond(EXPECTED_ISSUE_TIME), + expirationTimestamp = Instant.fromEpochSeconds(EXPECTED_EXPIRATION_TIME), + issueTimestamp = Instant.fromEpochSeconds(EXPECTED_ISSUE_TIME), externalAuthorizationIDsEndpoint = "http://localhost:8888/appList", thirdPartyAuthorizationEndpoint = "http://localhost:8888/thirdPartyAuth" ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt index 76098a51..adbd4f10 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt @@ -31,12 +31,12 @@ import io.mockk.mockk import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test -import java.time.Duration -import java.time.Instant +import kotlin.time.Duration.Companion.hours @OptIn(ExperimentalCoroutinesApi::class) class IdpBasicUseCaseTest { @@ -51,7 +51,7 @@ class IdpBasicUseCaseTest { private lateinit var useCase: IdpBasicUseCase - private val now = Instant.now() + private val now = Clock.System.now() private val idpConfigNow = IdpData.IdpConfiguration( authorizationEndpoint = "", ssoEndpoint = "", @@ -61,7 +61,7 @@ class IdpBasicUseCaseTest { pukIdpEncEndpoint = "", pukIdpSigEndpoint = "", certificate = mockk(), - expirationTimestamp = now.plus(Duration.ofHours(24)), + expirationTimestamp = now + 24.hours, issueTimestamp = now, externalAuthorizationIDsEndpoint = "", thirdPartyAuthorizationEndpoint = "" @@ -93,7 +93,7 @@ class IdpBasicUseCaseTest { fun `checkIdpConfigurationValidity - document expired - throws exception`() = runTest { useCase.checkIdpConfigurationValidity( idpConfigNow, - now.plus(Duration.ofHours(25)) // account for clock skew + now + 25.hours // account for clock skew ) } @@ -101,7 +101,7 @@ class IdpBasicUseCaseTest { fun `checkIdpConfigurationValidity - document expires too late - throws exception`() = runTest { useCase.checkIdpConfigurationValidity( idpConfigNow.copy( - expirationTimestamp = now.plus(Duration.ofHours(25)) + expirationTimestamp = now + 25.hours ), now ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt index a20a1344..79e8c3d9 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt @@ -49,8 +49,8 @@ import io.realm.kotlin.RealmConfiguration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock import org.junit.Rule -import java.time.Instant import kotlin.test.Test import kotlin.test.BeforeTest import kotlin.test.assertEquals @@ -295,7 +295,7 @@ class ProfilesRepositoryTest : TestDB() { @Test fun `update last authenticated`() = runTest { - val now = Instant.now() + val now = Clock.System.now() repo.saveProfile(defaultProfileName, true) repo.profiles().first().also { assertEquals(null, it[0].lastAuthenticated) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt index 36296c89..bd123952 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/settings/repository/SettingsRepositoryTest.kt @@ -33,11 +33,9 @@ import io.realm.kotlin.RealmConfiguration import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import org.junit.Rule import kotlin.test.Test -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset import kotlin.test.BeforeTest import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -85,16 +83,12 @@ class SettingsRepositoryTest : TestDB() { assertEquals(false, it.zoomEnabled) assertEquals(false, it.mlKitAccepted) assertEquals(false, it.userHasAcceptedInsecureDevice) - assertEquals( - LocalDateTime.of(2021, 10, 15, 0, 0).toInstant(ZoneOffset.UTC), - it.dataProtectionVersionAcceptedOn - ) assertEquals(0, it.authenticationFails) } repo.acceptInsecureDevice() - repo.acceptUpdatedDataTerms(Instant.ofEpochSecond(123456)) + repo.acceptUpdatedDataTerms(Instant.fromEpochSeconds(123456)) repo.incrementNumberOfAuthenticationFailures() repo.incrementNumberOfAuthenticationFailures() @@ -106,7 +100,6 @@ class SettingsRepositoryTest : TestDB() { assertEquals(true, it.zoomEnabled) assertEquals(true, it.mlKitAccepted) assertEquals(true, it.userHasAcceptedInsecureDevice) - assertEquals(Instant.ofEpochSecond(123456), it.dataProtectionVersionAcceptedOn) assertEquals(2, it.authenticationFails) } @@ -114,7 +107,6 @@ class SettingsRepositoryTest : TestDB() { repo.general.first().also { assertEquals(true, it.zoomEnabled) assertEquals(true, it.userHasAcceptedInsecureDevice) - assertEquals(Instant.ofEpochSecond(123456), it.dataProtectionVersionAcceptedOn) assertEquals(0, it.authenticationFails) } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt index 543bf029..03a10e40 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/OCSPUtilsTest.kt @@ -24,7 +24,7 @@ import org.bouncycastle.util.encoders.Base64 import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import kotlin.test.Test -import java.time.Duration +import kotlin.time.Duration.Companion.hours class OCSPUtilsTest { @Test @@ -43,23 +43,23 @@ class OCSPUtilsTest { fun `valid ocsp response cert is valid within 12 hours`() { val ocspResp = OCSPResp(Base64.decode(TestCertificates.OCSP1.Base64)).responseObject as BasicOCSPResp - ocspResp.checkValidity(Duration.ofHours(12), TestCertificates.OCSP1.ProducedAt.plus(Duration.ofHours(0))) - ocspResp.checkValidity(Duration.ofHours(12), TestCertificates.OCSP1.ProducedAt.plus(Duration.ofHours(5))) - ocspResp.checkValidity(Duration.ofHours(12), TestCertificates.OCSP1.ProducedAt.plus(Duration.ofHours(12))) + ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.plus(0.hours)) + ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.plus(5.hours)) + ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.plus(12.hours)) } @Test(expected = Exception::class) fun `valid ocsp response cert is invalid over 12 hours - throws exception`() { val ocspResp = OCSPResp(Base64.decode(TestCertificates.OCSP1.Base64)).responseObject as BasicOCSPResp - ocspResp.checkValidity(Duration.ofHours(12), TestCertificates.OCSP1.ProducedAt.plus(Duration.ofHours(13))) + ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.plus(13.hours)) } @Test(expected = Exception::class) fun `valid ocsp response cert is invalid if current time is in the past - throws exception`() { val ocspResp = OCSPResp(Base64.decode(TestCertificates.OCSP1.Base64)).responseObject as BasicOCSPResp - ocspResp.checkValidity(Duration.ofHours(12), TestCertificates.OCSP1.ProducedAt.minus(Duration.ofHours(1))) + ocspResp.checkValidity(12.hours, TestCertificates.OCSP1.ProducedAt.minus(1.hours)) } @Test diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt index 95bdbae6..f8bbf50a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/vau/TestData.kt @@ -22,13 +22,13 @@ package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList +import kotlinx.datetime.Instant import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okio.ByteString.Companion.decodeBase64 import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.SecureRandom -import java.time.Instant val BCProvider = BouncyCastleProvider() @@ -75,9 +75,9 @@ object TestCertificates { val CertList: UntrustedCertList by lazy { Json.decodeFromString(JsonCertList) } - val ValidTimestamp: Instant = Instant.ofEpochSecond(1615368104) // 2021-03-10T09:21:44.000Z + val ValidTimestamp: Instant = Instant.fromEpochSeconds(1615368104) // 2021-03-10T09:21:44.000Z val ExpiredTimestamp: Instant = - Instant.ofEpochSecond(1899364896) // 2030-03-10T09:21:36.812Z + Instant.fromEpochSeconds(1899364896) // 2030-03-10T09:21:36.812Z } object Idp1 { @@ -160,7 +160,7 @@ object TestCertificates { object OCSP1 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAxWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcDrUjkAJSNgAAYDzIwMjEwNTE3MDYyMzAxWqARGA8yMDIxMDUxNzA2MjMwMVqhIzAhMB8GCSsGAQUFBzABAgQSBBDkdyImUBsO+Q8iAA2xbXu8MAkGByqGSM49BAEDRwAwRAIgW+JlwUmnZCVsME2kOyQlcqF01Lel/0nQdE6IaZmFADECIGhOH1k5Dzq42y2jCxZCzxevRc6vY1o8ky0Xy4DxLIWJoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232581) // 2021-05-17T08:23:01.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232581) // 2021-05-17T08:23:01.000+0200 val CertToCheckSerialNumber = "1034953504625805" // IDP 1 object SignerCert { @@ -175,7 +175,7 @@ object TestCertificates { object OCSP2 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAxWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBuyypCGo7gAAYDzIwMjEwNTE3MDYyMzAxWqARGA8yMDIxMDUxNzA2MjMwMVqhIzAhMB8GCSsGAQUFBzABAgQSBBDIsivTG9WljP4InmqVdKQmMAkGByqGSM49BAEDRwAwRAIgZMCyRhqMOaEG10KPz3mL5Yh7oX9fiIdBl8WrxLT2SewCIEvjzedVlnbt/j4e7VALo2xl8wvOcYe8gT04+PqH5vkfoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232581) // 2021-05-17T08:23:01.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232581) // 2021-05-17T08:23:01.000+0200 val CertToCheckSerialNumber = "487275465566779" // IDP 2 } @@ -185,7 +185,7 @@ object TestCertificates { object OCSP3 { const val Base64 = "MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAwWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBPCti7yC3gAAYDzIwMjEwNTE3MDYyMzAwWqARGA8yMDIxMDUxNzA2MjMwMFqhIzAhMB8GCSsGAQUFBzABAgQSBBAWpjYsPzj/U96/S1MvypTWMAkGByqGSM49BAEDRwAwRAIgXfEC3h/1H2/aHGEyJY9L59S6NbqdkStBBk2vczj+3mwCIASMGDqPuhA7ZLBJ5HhHpwKYEQw/YPluyBMnz7j2dXtPoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" - val ProducedAt = Instant.ofEpochSecond(1621232580) // 2021-05-17T08:23:00.000+0200 + val ProducedAt = Instant.fromEpochSeconds(1621232580) // 2021-05-17T08:23:00.000+0200 val CertToCheckSerialNumber = "347632017809591" // VAU } diff --git a/common/src/commonTest/resources/audit_events_bundle_version_1_2.json b/common/src/commonTest/resources/audit_events_bundle_version_1_2.json new file mode 100644 index 00000000..38bbf308 --- /dev/null +++ b/common/src/commonTest/resources/audit_events_bundle_version_1_2.json @@ -0,0 +1,84 @@ +{ + "id": "bca172dc-495c-4e19-9c7b-7977739d9ce1", + "type": "searchset", + "timestamp": "2022-08-17T12:59:27.432+00:00", + "resourceType": "Bundle", + "total": 1, + "entry": [ + { + "fullUrl": "https://example.com/AuditEvent/01eb7f56-6820-a140-abdb-34aa9f2ab6ea", + "resource": { + "resourceType": "AuditEvent", + "id": "9361863d-fec0-4ba9-8776-7905cf1b0cfa", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_AuditEvent|1.2" + ], + "tag": [ + { + "display": "AuditEvent entry generated by E-Rezept-Backend-Service on access to any patient data by any person" + } + ] + }, + "type": { + "system": "http://terminology.hl7.org/CodeSystem/audit-event-type", + "code": "rest" + }, + "source": { + "site": "E-Rezept Fachdienst", + "observer": { + "reference": "Device/1" + } + }, + "text": { + "status": "generated", + "div": "

Praxis Dr. Müller, Bahnhofstr. 78 hat ein E-Rezept 160.123.456.789.123.58 eingestellt
" + }, + "subtype": [ + { + "system": "http://hl7.org/fhir/restful-interaction", + "code": "create" + } + ], + "language": "de", + "action": "C", + "recorded": "2022-04-27T08:04:27.434+00:00", + "outcome": "0", + "agent": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/extra-security-role-type", + "code": "humanuser", + "display": "Human User" + } + ] + }, + "who": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "1-SMC-B-Testkarte-883110000095957" + } + }, + "name": "Praxis Dr. Müller", + "requestor": false + } + ], + "entity": [ + { + "what": { + "reference": "https://erp.app.ti-dienste.de/Task/160.123.456.789.123.58", + "identifier": { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.123.456.789.123.58" + } + }, + "name": "X234567890", + "description": "160.123.456.789.123.58" + } + ] + } + } + ] +} diff --git a/common/src/commonTest/resources/communications_bundle_version_1_2.json b/common/src/commonTest/resources/communications_bundle_version_1_2.json new file mode 100644 index 00000000..03aa6739 --- /dev/null +++ b/common/src/commonTest/resources/communications_bundle_version_1_2.json @@ -0,0 +1,74 @@ +{ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "resource": { + "resourceType": "Communication", + "id": "7977a4ab-97a9-4d95-afb3-6c4c1e2ac596", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Communication_Reply|1.2" + ], + "tag": [ + { + "display": "Communication message sent by pharmacy to patient in response to a previous Task-related message" + } + ] + }, + "status": "unknown", + "basedOn": [ + { + "reference": "Task/160.000.033.491.280.78" + } + ], + "sender": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-SMC-B-Testkarte-883110000123465" + } + }, + "recipient": [ + { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X234567890" + } + } + ], + "sent": "2020-04-29T13:46:30.128+02:00", + "payload": [ + { + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AvailabilityState", + "valueCoding": { + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_AvailabilityStatus", + "code": "20" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_SupplyOptionsType", + "extension": [ + { + "url": "onPremise", + "valueBoolean": true + }, + { + "url": "shipment", + "valueBoolean": false + }, + { + "url": "delivery", + "valueBoolean": true + } + ] + } + ], + "contentString": "Eisern" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/bundle_med_dispense_version_1_2.json b/common/src/commonTest/resources/fhir/bundle_med_dispense_version_1_2.json new file mode 100644 index 00000000..7cf9f12f --- /dev/null +++ b/common/src/commonTest/resources/fhir/bundle_med_dispense_version_1_2.json @@ -0,0 +1,249 @@ +{ + "resourceType": "Bundle", + "id": "9145d0d0-7b77-483f-ad89-cd9d34fc1f08", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_CloseOperationInputBundle|1.2" + ], + "tag": [ + { + "display": "MedicationDispense Bundle for $close-Operation on dispensation of multiple medications" + } + ] + }, + "type": "collection", + "entry": [ + { + "fullUrl": "http://hier-koennte-ihre-werbung-stehen", + "resource": { + "resourceType": "MedicationDispense", + "id": "3465270a-11e7-4bbf-ae53-378f9cc52747", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.2" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "001413e4-a5e9-48da-9b07-c17bab476407", + "meta": { + "profile": [ + "https://fhir.kbv.de/StructureDefinition/KBV_PR_ERP_Medication_PZN|1.1.0" + ] + }, + "extension": [ + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_Base_Medication_Type", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20220331", + "code": "763158003", + "display": "Medicinal product (product)" + } + ] + } + }, + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Category", + "valueCoding": { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_ERP_Medication_Category", + "code": "00" + } + }, + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Vaccine", + "valueBoolean": false + }, + { + "url": "http://fhir.de/StructureDefinition/normgroesse", + "valueCode": "N1" + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06313728" + } + ], + "text": "Sumatriptan-1a Pharma 100 mg Tabletten" + }, + "form": { + "coding": [ + { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM", + "code": "TAB" + } + ] + }, + "amount": { + "numerator": { + "unit": "St", + "extension": [ + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_PackagingSize", + "valueString": "20 St." + } + ] + }, + "denominator": { + "value": 1 + } + } + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.033.491.280.78" + } + ], + "status": "completed", + "medicationReference": { + "reference": "#001413e4-a5e9-48da-9b07-c17bab476407" + }, + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X234567890" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-abc-1234567890" + } + } + } + ], + "whenHandedOver": "2022-02-28", + "dosageInstruction": [ + { + "text": "1-0-1-0" + } + ] + } + }, + { + "fullUrl": "http://waltraud-was-here", + "resource": { + "resourceType": "MedicationDispense", + "id": "3465270a-11e7-4bbf-ae53-378f9cc52747", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_MedicationDispense|1.2" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "001413e4-a5e9-48da-9b07-c17bab476407", + "meta": { + "profile": [ + "https://fhir.kbv.de/StructureDefinition/KBV_PR_ERP_Medication_PZN|1.1.0" + ] + }, + "extension": [ + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_Base_Medication_Type", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20220331", + "code": "763158003", + "display": "Medicinal product (product)" + } + ] + } + }, + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Category", + "valueCoding": { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_ERP_Medication_Category", + "code": "00" + } + }, + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Vaccine", + "valueBoolean": false + }, + { + "url": "http://fhir.de/StructureDefinition/normgroesse", + "valueCode": "N1" + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06313728" + } + ], + "text": "Sumatriptan-1a Pharma 100 mg Tabletten" + }, + "form": { + "coding": [ + { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM", + "code": "TAB" + } + ] + }, + "amount": { + "numerator": { + "unit": "St", + "extension": [ + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_PackagingSize", + "valueString": "20 St." + } + ] + }, + "denominator": { + "value": 1 + } + } + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.033.491.280.78" + } + ], + "status": "completed", + "medicationReference": { + "reference": "#001413e4-a5e9-48da-9b07-c17bab476407" + }, + "subject": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X234567890" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/sid/telematik-id", + "value": "3-abc-1234567890" + } + } + } + ], + "whenHandedOver": "2022-02-28", + "dosageInstruction": [ + { + "text": "1-0-1-0" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/medication_dispense_without_category.json b/common/src/commonTest/resources/fhir/medication_dispense_without_category.json new file mode 100644 index 00000000..6bcb9e68 --- /dev/null +++ b/common/src/commonTest/resources/fhir/medication_dispense_without_category.json @@ -0,0 +1,94 @@ +{ + "resourceType": "MedicationDispense", + "id": "160.000.000.031.686.59", + "meta": { + "profile": [ + "https://gematik.de/fhir/StructureDefinition/ErxMedicationDispense" + ] + }, + "contained": [ + { + "resourceType": "Medication", + "id": "65cab1df-2514-4395-910f-dc4e247a84ca", + "meta": { + "profile": [ + "https://fhir.kbv.de/StructureDefinition/KBV_PR_ERP_Medication_PZN|1.0.2" + ] + }, + "extension": [ + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Category", + "valueCoding": { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_ERP_Medication_Category", + "code": "05" + } + }, + { + "url": "https://fhir.kbv.de/StructureDefinition/KBV_EX_ERP_Medication_Vaccine", + "valueBoolean": false + }, + { + "url": "http://fhir.de/StructureDefinition/normgroesse", + "valueCode": "Sonstiges" + } + ], + "code": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/ifa/pzn", + "code": "06491772" + } + ], + "text": "Defamipin" + }, + "form": { + "coding": [ + { + "system": "https://fhir.kbv.de/CodeSystem/KBV_CS_SFHIR_KBV_DARREICHUNGSFORM", + "code": "FET" + } + ] + }, + "amount": { + "numerator": { + "value": 18, + "unit": "Stk" + }, + "denominator": { + "value": 1 + } + }, + "batch": { + "lotNumber": "8521037577", + "expirationDate": "2023-05-02T08:26:06+02:00" + } + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/NamingSystem/PrescriptionID", + "value": "160.000.000.031.686.59" + } + ], + "status": "completed", + "medicationReference": { + "reference": "#65cab1df-2514-4395-910f-dc4e247a84ca" + }, + "subject": { + "identifier": { + "system": "http://fhir.de/NamingSystem/gkv/kvid-10", + "value": "X110535541" + } + }, + "performer": [ + { + "actor": { + "identifier": { + "system": "https://gematik.de/fhir/NamingSystem/TelematikID", + "value": "3-SMC-B-Testkarte-883110000116873" + } + } + } + ], + "whenHandedOver": "2022-07-12" +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/patient_incomplete_birth_date.json b/common/src/commonTest/resources/fhir/patient_incomplete_birth_date.json new file mode 100644 index 00000000..3a6a1b05 --- /dev/null +++ b/common/src/commonTest/resources/fhir/patient_incomplete_birth_date.json @@ -0,0 +1,87 @@ +{ + "resourceType": "Patient", + "id": "fc0d145b-09b4-4af6-b477-935c1862ac7f", + "meta": { + "profile": [ + "https://fhir.kbv.de/StructureDefinition/KBV_PR_FOR_Patient|1.0.3" + ] + }, + "identifier": [ + { + "type": { + "coding": [ + { + "system": "http://fhir.de/CodeSystem/identifier-type-de-basis", + "code": "GKV" + } + ] + }, + "system": "http://fhir.de/NamingSystem/gkv/kvid-10", + "value": "X110535541" + } + ], + "name": [ + { + "use": "official", + "family": "Graf Freiherr von Schinder", + "_family": { + "extension": [ + { + "url": "http://fhir.de/StructureDefinition/humanname-namenszusatz", + "valueString": "Graf Freiherr" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix", + "valueString": "von" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/humanname-own-name", + "valueString": "Schinder" + } + ] + }, + "given": [ + "Lars" + ], + "prefix": [ + "Prinzessin" + ], + "_prefix": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-EN-qualifier", + "valueCode": "AC" + } + ] + } + ] + } + ], + "birthDate": "1964", + "address": [ + { + "type": "both", + "line": [ + "Siegburger Str. 155" + ], + "_line": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber", + "valueString": "155" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName", + "valueString": "Siegburger Str." + } + ] + } + ], + "city": "Köln", + "postalCode": "51105", + "country": "D" + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/task_bundle_vers_1_2.json b/common/src/commonTest/resources/fhir/task_bundle_vers_1_2.json new file mode 100644 index 00000000..32259e81 --- /dev/null +++ b/common/src/commonTest/resources/fhir/task_bundle_vers_1_2.json @@ -0,0 +1,101 @@ +{ + "resourceType": "Bundle", + "type": "searchset", + "entry": [ + { + "resource": { + "resourceType": "Task", + "id": "607255ed-ce41-47fc-aad3-cfce1c39963f", + "meta": { + "profile": [ + "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.2" + ], + "tag": [ + { + "display": "Task in READY state activated by (Z)PVS/KIS via $activate operation" + } + ] + }, + "intent": "order", + "extension": [ + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_PrescriptionType", + "valueCoding": { + "code": "160", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_FlowType", + "display": "Muster 16 (Apothekenpflichtige Arzneimittel)" + } + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_AcceptDate", + "valueDate": "2022-04-02" + }, + { + "url": "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_EX_ExpiryDate", + "valueDate": "2022-06-02" + } + ], + "identifier": [ + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "160.000.033.491.280.78" + }, + { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_AccessCode", + "value": "777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea" + } + ], + "status": "ready", + "authoredOn": "2022-03-18T15:26:00+00:00", + "performerType": [ + { + "coding": [ + { + "code": "urn:oid:1.2.276.0.76.4.54", + "system": "urn:ietf:rfc:3986", + "display": "Öffentliche Apotheke" + } + ] + } + ], + "for": { + "identifier": { + "system": "http://fhir.de/sid/gkv/kvid-10", + "value": "X123456789" + } + }, + "lastModified": "2022-03-18T15:27:00+00:00", + "input": [ + { + "type": { + "coding": [ + { + "code": "1", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_DocumentType", + "display": "Health Care Provider Prescription" + } + ] + }, + "valueReference": { + "reference": "281a985c-f25b-4aae-91a6-41ad744080b0" + } + }, + { + "type": { + "coding": [ + { + "code": "2", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_DocumentType", + "display": "Patient Confirmation" + } + ] + }, + "valueReference": { + "reference": "f8c2298f-7c00-4a68-af29-8a2862d55d43" + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/common/src/commonTest/resources/fhir/task_vers_1_2.json b/common/src/commonTest/resources/fhir/task_vers_1_2.json index f524d471..6d4294e9 100644 --- a/common/src/commonTest/resources/fhir/task_vers_1_2.json +++ b/common/src/commonTest/resources/fhir/task_vers_1_2.json @@ -1,13 +1,13 @@ { "resourceType": "Task", - "id": "607255ed-ce41-47fc-aad3-cfce1c39963f", + "id": "09330307-16ce-4cdc-810a-ca24ef80dde3", "meta": { "profile": [ "https://gematik.de/fhir/erp/StructureDefinition/GEM_ERP_PR_Task|1.2" ], "tag": [ { - "display": "Task in READY state activated by (Z)PVS/KIS via $activate operation" + "display": "Task in COMPLETED state dispensed by pharmacy via $closed operation" } ] }, @@ -40,7 +40,7 @@ "value": "777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea" } ], - "status": "ready", + "status": "completed", "authoredOn": "2022-03-18T15:26:00+00:00", "performerType": [ { @@ -59,7 +59,7 @@ "value": "X123456789" } }, - "lastModified": "2022-03-18T15:27:00+00:00", + "lastModified": "2022-03-18T15:29:00+00:00", "input": [ { "type": { @@ -89,5 +89,21 @@ "reference": "f8c2298f-7c00-4a68-af29-8a2862d55d43" } } + ], + "output": [ + { + "type": { + "coding": [ + { + "code": "3", + "system": "https://gematik.de/fhir/erp/CodeSystem/GEM_ERP_CS_DocumentType", + "display": "Receipt" + } + ] + }, + "valueReference": { + "reference": "dffbfd6a-5712-4798-bdc8-07201eb77ab8" + } + } ] } \ No newline at end of file diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 948d6b3e..fe00941e 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -86,7 +86,6 @@ MagicNumber:FileIdentifier.kt$FileIdentifier$0x3FFF MagicNumber:FileIdentifier.kt$FileIdentifier$0xFEFF MagicNumber:GeneralAuthenticateCommand.kt$28 - MagicNumber:HealthCardOrderComponents.kt$9 MagicNumber:HealthCardVersion2.kt$16 MagicNumber:HealthCardVersion2.kt$8 MagicNumber:HealthCardVersion2.kt$HealthCardVersion2.Companion$3 @@ -102,12 +101,10 @@ MagicNumber:IdpBasicUseCase.kt$4 MagicNumber:IdpBasicUseCase.kt$IdpBasicUseCase$60 MagicNumber:IdpBasicUseCase.kt$IdpNonce.Companion$32 - MagicNumber:IdpData.kt$24 MagicNumber:IdpUseCase.kt$IdpUseCase$400 MagicNumber:IdpUseCase.kt$IdpUseCase$401 MagicNumber:IdpUseCase.kt$IdpUseCase$403 MagicNumber:JWTExtensions.kt$64 - MagicNumber:LocalDataSource.kt$AuditEventLocalDataSource$3 MagicNumber:LoginWithHealthCardScreen.kt$1000 MagicNumber:LoginWithHealthCardScreen.kt$3 MagicNumber:LoginWithHealthCardScreen.kt$4 @@ -134,11 +131,6 @@ MagicNumber:PasswordScreen.kt$3 MagicNumber:PasswordScreen.kt$4 MagicNumber:PharmacySearchModel.kt$Location$180.0 - MagicNumber:PrescriptionScreenComponents.kt$20 - MagicNumber:PrescriptionScreenComponents.kt$21 - MagicNumber:PrescriptionScreenComponents.kt$5L - MagicNumber:PrescriptionScreenComponents.kt$60L - MagicNumber:PrescriptionScreenComponents.kt$97 MagicNumber:PrescriptionViewModel.kt$PrescriptionViewModel$1000L MagicNumber:PrescriptionViewModel.kt$PrescriptionViewModel$60L MagicNumber:ProtocolUseCase.kt$ProtocolUseCase$50 @@ -428,7 +420,6 @@ UnusedPrivateMember:TestResource.kt$TestResource.Companion$private const val ID_PIN_CH = 1 UnusedPrivateMember:Theme.kt$private val LocalAppTypographyColors = staticCompositionLocalOf<AppTypographyColors> { error("No AppTypographyColors provided") } UnusedPrivateMember:TruststoreUseCase.kt$private const val RCA_PREFIX = "GEM.RCA" - UnusedPrivateMember:TwoDCodeValidatorTest.kt$TwoDCodeValidatorTest$private val notWellFormatted = ScannedCode( "{\n" + " \"urls\": [\n" + " \"Task/234fabe0964598efd23f34dd23e122b2323344ea8e8934dae23e2a9a934513bc/\$accept?ac=777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea\",\n" + " \"Task/5e78f21cd6abc35edf4f1726c3d451ea2736d547a263f45726bc13a47e65d189/\$accept?ac=d3e6092ae3af14b5225e2ddbe5a4f59b3939a907d6fdd5ce6a760ca71f45d8e5\",\n" + " ]\n" + "}", Instant.now() ) VariableNaming:CommandApduTest.kt$CommandApduTest$val header_plus_lc = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x00, (dataSize shr 8).toByte(), dataSize.toByte()) VariableNaming:CommandApduTest.kt$CommandApduTest$val header_plus_lc = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0xFF.toByte()) VariableNaming:CommandApduTest.kt$CommandApduTest$val header_plus_lc = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x00, 0x01, 0x00) diff --git a/gradle.properties b/gradle.properties index 5ca0c25a..723d3063 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.7.0 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.8.0 GMTIK/eRezeptApp # DATA_PROTECTION_LAST_UPDATED = 2022-01-06 # @@ -38,51 +38,10 @@ TEST_INSTRUMENTATION_ORCHESTRATOR=androidx.test.runner.AndroidJUnitRunner #PROD Environment IDP_SERVICE_URI_PU=https://idp.app.ti-dienste.de/.well-known/ BASE_SERVICE_URI_PU=https://erp.app.ti-dienste.de/ -# PU -ERP_API_KEY_GOOGLE_PU= -# PU Huawei -ERP_API_KEY_HUAWEI_PU= -# PU Desktop -ERP_API_KEY_DESKTOP_PU= -# -#TEST Ref Environment -IDP_SERVICE_URI_TR= -BASE_SERVICE_URI_TR= -# TR -ERP_API_KEY_GOOGLE_TR= -# TR Huawei -ERP_API_KEY_HUAWEI_TR= -# -#TEST Environment -IDP_SERVICE_URI_TU= -BASE_SERVICE_URI_TU= -# TU -ERP_API_KEY_GOOGLE_TU= -# TU Huawei -ERP_API_KEY_HUAWEI_TU= -# TU Desktop -ERP_API_KEY_DESKTOP_TU= -# -#REF Environment -IDP_SERVICE_URI_RU= -BASE_SERVICE_URI_RU= -# RU -ERP_API_KEY_GOOGLE_RU= -# RU Huawei -ERP_API_KEY_HUAWEI_RU= -# RU Desktop -ERP_API_KEY_DESKTOP_RU= - -#REF-DEV Environment -IDP_SERVICE_URI_RU_DEV= -BASE_SERVICE_URI_RU_DEV= # # Pharmacy service # PHARMACY_SERVICE_URI=https://apovzd.app.ti-dienste.de/ -PHARMACY_API_KEY= -PHARMACY_SERVICE_URI_TEST= -PHARMACY_API_KEY_TEST= # # VAU # @@ -90,14 +49,7 @@ VAU_OCSP_RESPONSE_MAX_AGE=12 # #GemRootCa3 APP_TRUST_ANCHOR_BASE64=MIICaTCCAg+gAwIBAgIBATAKBggqhkjOPQQDAjBtMQswCQYDVQQGEwJERTEVMBMGA1UECgwMZ2VtYXRpayBHbWJIMTQwMgYDVQQLDCtaZW50cmFsZSBSb290LUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMREwDwYDVQQDDAhHRU0uUkNBMzAeFw0xNzEwMTcwNzEzMDNaFw0yNzEwMTUwNzEzMDNaMG0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxnZW1hdGlrIEdtYkgxNDAyBgNVBAsMK1plbnRyYWxlIFJvb3QtQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxETAPBgNVBAMMCEdFTS5SQ0EzMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABFhZKSE0xvaeHzZB0A7sRYwIphEWYk/+uFw4kOLnBt2kbP4P7L0lFOQfp6W0a2lCcmKk+k25VHrj7PCMyV/AVdqjgZ4wgZswHQYDVR0OBBYEFN/DvnW+JesTMjAup1CFCJ83ENDoMEIGCCsGAQUFBwEBBDYwNDAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Aucm9vdC1jYS50aS1kaWVuc3RlLmRlL29jc3AwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwFQYDVR0gBA4wDDAKBggqghQATASBIzAKBggqhkjOPQQDAgNIADBFAiEAnOlHsQ5tQ2HPoKVngVQnbvVGteLyMSEnNGbYegnfPFECIEUlFmjATBNklr35xvWQPZUMdIsy7SzUulwFDodpdGr/ -#GemRootCA3TestOnly -APP_TRUST_ANCHOR_BASE64_TEST= # DEBUG_TEST_IDS_ENABLED=true # -SAFETYNET_API_KEY= -# -DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE= -DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY= -# -MAPS_API_KEY= +MAPS_API_KEY=MAPS_API_KEY \ No newline at end of file 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 eacc896c..b85d1100 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 @@ -83,6 +83,7 @@ class AppDependenciesPlugin : Plugin { object KotlinX { fun coroutines(target: String) = "org.jetbrains.kotlinx:kotlinx-coroutines-$target:1.6.4" + const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" object Test { val coroutinesTest = coroutines("test") } @@ -141,6 +142,7 @@ class AppDependenciesPlugin : Plugin { const val core = "androidx.test:core:1.5.0" const val rules = "androidx.test:rules:1.5.0" const val espresso = "androidx.test.espresso:espresso-core:3.5.0" + const val espressoIntents = "androidx.test.espresso:espresso-intents:3.5.0" const val junitExt = "androidx.test.ext:junit:1.1.4" const val navigation = "androidx.navigation:navigation-testing:2.5.3" } @@ -209,6 +211,10 @@ class AppDependenciesPlugin : Plugin { const val zxcvbn = "com.nulab-inc:zxcvbn:1.7.0" } + object ContentSquare { + const val cts = "com.contentsquare.android:library:4.12.0" + } + object Test { fun mockk(module: String) = "io.mockk:$module:1.13.2" const val junit4 = "junit:junit:4.13.2" @@ -273,6 +279,9 @@ object App { fun passwordStrength(init: AppDependenciesPlugin.Dependencies.PasswordStrength.() -> Unit) = AppDependenciesPlugin.Dependencies.PasswordStrength.init() + fun contentSquare(init: AppDependenciesPlugin.Dependencies.ContentSquare.() -> Unit) = + AppDependenciesPlugin.Dependencies.ContentSquare.init() + fun test(init: AppDependenciesPlugin.Dependencies.Test.() -> Unit) = AppDependenciesPlugin.Dependencies.Test.init() diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt index 39ae3db3..fd4e389d 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt @@ -20,7 +20,6 @@ package de.gematik.ti.erp import org.gradle.api.Project import org.gradle.kotlin.dsl.getPlugin -import org.gradle.kotlin.dsl.provideDelegate import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty @@ -28,8 +27,7 @@ fun Project.overriding() = PropertyDelegateProvider { _: Any?, _ -> ReadOnlyProperty { ref, property -> project.plugins.getPlugin(AppDependenciesPlugin::class).overrideProperties.getProperty(property.name) - ?: run { - project.provideDelegate(null, property).getValue(null, property) as String - } + ?: (project.properties[property.name] as? String) + ?: "" } }