diff --git a/.gitignore b/.gitignore index 6d16126f..a6aa9bab 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ documentation-internal android/src/androidTest technical_requirements_report.html keystore +CODEOWNERS +local.cliactions.yaml diff --git a/README.md b/README.md index fa3c595b..21851be0 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,30 @@ This repository is an [Kotlin Multiplatform Project](https://kotlinlang.org/docs ### Structure ```text -|-- android -| `-- src -| |-- androidTest -| |-- debug -| |-- main -| |-- release -| |-- sharedTest -| `-- test +|-- app +| `-- android +| `-- src +| |-- androidTest +| |-- main +| `-- test +| `-- android-mock +| `-- src +| |-- androidTest +| |-- main +| `-- test +| `-- demo-mode +| `-- src +| |-- main +| `-- features +| `-- src +| |-- debug +| |-- release +| |-- androidTest +| |-- main +| `-- test +| `-- shared-test +| `-- src +| |-- main |-- common | `-- src | |-- androidMain @@ -95,7 +111,7 @@ gradle :android:assemble(Google|Huawei)Pu(External|Internal)(Debug|Release) -Pbu *Note: Currently the android build variant is derived from the `buildkonfig.flavor` property.* -The resulting `.apk` can be found in e.g. `android/build/outputs/apk/googlePuExternal/debug/`. +The resulting `.apk` can be found in e.g. `app/android/build/outputs/apk/googlePuExternal/debug/`. #### Visualize Test Tags diff --git a/ReleaseNotes.md b/ReleaseNotes.md index eaeb76a5..36a8b5a6 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,9 @@ +# Release 1.18.1 +- Optimized performance +- UX improvements +- Bug fixes + + # Release 1.17.2 - Added Demo mode function - Added function to change the name of scanned prescriptions diff --git a/app/android-mock/build.gradle.kts b/app/android-mock/build.gradle.kts index 973500a7..a5364bed 100644 --- a/app/android-mock/build.gradle.kts +++ b/app/android-mock/build.gradle.kts @@ -78,6 +78,9 @@ android { resValue("string", "app_label", "E-Rezept Mock") versionNameSuffix = "-debug" } + create("minifiedDebug") { + initWith(debug) + } } packagingOptions { diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt index 8a327f4f..c28ca8ec 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt @@ -7,8 +7,10 @@ package de.gematik.ti.erp.app import androidx.lifecycle.ProcessLifecycleOwner import com.contentsquare.android.Contentsquare import com.tom_roush.pdfbox.android.PDFBoxResourceLoader -import de.gematik.ti.erp.app.di.allModules -import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase +import de.gematik.ti.erp.app.di.appModules +import de.gematik.ti.erp.app.di.mockFeatureModule +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.userauthentication.observer.ProcessLifecycleObserver import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import org.kodein.di.DI @@ -21,25 +23,26 @@ class DefaultErezeptMockApp : ErezeptApp(), DIAware { override val di by DI.lazy { import(androidXModule(this@DefaultErezeptMockApp)) - importAll(allModules) - bindSingleton { AuthenticationUseCase(instance()) } + importAll(appModules) + importAll(mockFeatureModule, allowOverride = true) + bindSingleton { InactivityTimeoutObserver(instance(), instance()) } + bindSingleton { ProcessLifecycleObserver(ProcessLifecycleOwner, instance()) } bindSingleton { VisibleDebugTree() } } - private val authUseCase: AuthenticationUseCase by instance() + private val processLifecycleObserver: ProcessLifecycleObserver by instance() private val visibleDebugTree: VisibleDebugTree by instance() override fun onCreate() { super.onCreate() - if (BuildKonfig.INTERNAL) { - Napier.base(DebugAntilog()) - Napier.base(visibleDebugTree) - } - ProcessLifecycleOwner.get().lifecycle.apply { - addObserver(authUseCase) - } + Napier.base(DebugAntilog()) + Napier.base(visibleDebugTree) + + processLifecycleObserver.observeForInactivity() + PDFBoxResourceLoader.init(this) + Contentsquare.start(this) } } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt similarity index 64% rename from app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt index b5d322a4..b013efdb 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt @@ -1,33 +1,3 @@ -package de.gematik.ti.erp.app.di - -import android.content.Context -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.Analytics -import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase -import de.gematik.ti.erp.app.attestation.usecase.integrityModule -import de.gematik.ti.erp.app.cardunlock.cardUnlockModule -import de.gematik.ti.erp.app.cardwall.cardWallModule -import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager -import de.gematik.ti.erp.app.idp.idpModule -import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule -import de.gematik.ti.erp.app.orders.messagesModule -import de.gematik.ti.erp.app.pharmacy.pharmacyMockModule -import de.gematik.ti.erp.app.pkv.pkvModule -import de.gematik.ti.erp.app.prescription.prescriptionModule -import de.gematik.ti.erp.app.prescription.taskModule -import de.gematik.ti.erp.app.profiles.profilesModule -import de.gematik.ti.erp.app.protocol.protocolModule -import de.gematik.ti.erp.app.redeem.redeemModule -import de.gematik.ti.erp.app.settings.settingsModule -import de.gematik.ti.erp.app.vau.vauModule -import org.kodein.di.DI -import org.kodein.di.bindProvider -import org.kodein.di.bindSingleton -import org.kodein.di.instance - /* * Copyright (c) 2024 gematik GmbH * @@ -46,6 +16,22 @@ import org.kodein.di.instance * */ +package de.gematik.ti.erp.app.di + +import android.content.Context +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.info.mockBuildConfigurationModule +import de.gematik.ti.erp.app.pkv.mockFileProviderAuthorityModule +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + private const val PREFERENCES_FILE_NAME = "appPrefs" private const val NETWORK_SECURE_PREFS_FILE_NAME = "networkingSecurePrefs" private const val NETWORK_PREFS_FILE_NAME = "networkingPrefs" @@ -55,12 +41,7 @@ const val ApplicationPreferencesTag = "ApplicationPreferences" const val NetworkPreferencesTag = "NetworkPreferences" const val NetworkSecurePreferencesTag = "NetworkSecurePreferences" -@Requirement( - "A_20184#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Bind EncryptedSharedPreferences." -) -val allModules = DI.Module("allModules") { +val appModules = DI.Module("appModules") { bindSingleton { object : DispatchProvider {} } bindSingleton(ApplicationPreferencesTag) { val context = instance() @@ -89,25 +70,10 @@ val allModules = DI.Module("allModules") { bindProvider { AnalyticsUseCase(instance()) } - bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag), instance()) } + bindSingleton { Analytics(instance(), instance(), instance(), instance()) } importAll( - cardWallModule, - integrityModule, - networkModule, - realmModule, - idpModule, - messagesModule, - orderHealthCardModule, - pharmacyMockModule, - redeemModule, - prescriptionModule, - profilesModule, - protocolModule, - taskModule, - settingsModule, - vauModule, - cardUnlockModule, - pkvModule + mockBuildConfigurationModule, + mockFileProviderAuthorityModule ) } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt new file mode 100644 index 00000000..370fede2 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/MockFeatureModule.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +import de.gematik.ti.erp.app.appsecurity.appSecurityModule +import de.gematik.ti.erp.app.authentication.di.authenticationModule +import de.gematik.ti.erp.app.cardunlock.cardUnlockModule +import de.gematik.ti.erp.app.cardwall.cardWallModule +import de.gematik.ti.erp.app.idp.idpModule +import de.gematik.ti.erp.app.idp.idpUseCaseModule +import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule +import de.gematik.ti.erp.app.orders.messageRepositoryModule +import de.gematik.ti.erp.app.orders.messagesModule +import de.gematik.ti.erp.app.pharmacy.di.pharmacyRepositoryModule +import de.gematik.ti.erp.app.pharmacy.pharmacyMockModule +import de.gematik.ti.erp.app.pkv.pkvModule +import de.gematik.ti.erp.app.prescription.prescriptionModule +import de.gematik.ti.erp.app.prescription.prescriptionRepositoryModule +import de.gematik.ti.erp.app.prescription.taskModule +import de.gematik.ti.erp.app.prescription.taskRepositoryModule +import de.gematik.ti.erp.app.profiles.profileRepositoryModule +import de.gematik.ti.erp.app.profiles.profilesModule +import de.gematik.ti.erp.app.protocol.protocolModule +import de.gematik.ti.erp.app.protocol.protocolRepositoryModule +import de.gematik.ti.erp.app.redeem.redeemModule +import de.gematik.ti.erp.app.settings.settingsModule +import de.gematik.ti.erp.app.vau.vauModule +import org.kodein.di.DI + +val mockFeatureModule = DI.Module("featureModule", allowSilentOverride = true) { + importAll( + cardWallModule, + appSecurityModule, + networkModule, + realmModule, + idpModule, + idpUseCaseModule, + messagesModule, + orderHealthCardModule, + redeemModule, + prescriptionModule, + profilesModule, + protocolModule, + taskModule, + settingsModule, + vauModule, + cardUnlockModule, + pkvModule, + authenticationModule, + profileRepositoryModule, + prescriptionRepositoryModule, + protocolRepositoryModule, + pharmacyRepositoryModule, + messageRepositoryModule, + taskRepositoryModule, + // mocked modules + pharmacyMockModule, + allowOverride = true + ) +} + + diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt new file mode 100644 index 00000000..5cc21580 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigInformation.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.info + +import android.content.Context +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.mock.BuildConfig +import java.util.Locale + +private const val DARK_THEME_ON = "an" +private const val DARK_THEME_OFF = "aus" +private const val RELEASE_CANDIDATE = "RC" +private const val SEPARATOR = "-" +private const val SPACE = " " + +class MockBuildConfigInformation : BuildConfigInformation { + + override fun versionName(): String { + BuildConfig.VERSION_NAME.split(RELEASE_CANDIDATE) + .takeIf { + it.size > 1 + }?.let { splits -> + val secondSplit = splits[1].split(SEPARATOR) + if (secondSplit.size > 1) { + // Removes the R from R1.20.23 and makes it 1.20.23 + val tag = splits[0].drop(1) + // Takes a 6z&hj4f58dzf9j0890hfj4938that509z97h and makes it 6z&hj4f58 + val truncatedCommitHash = secondSplit[1].take(9) + // RC-1 or RC-2 + val releaseCandidateNumber = secondSplit[0] + return listOf( + tag, + RELEASE_CANDIDATE, + SEPARATOR, + releaseCandidateNumber, + SEPARATOR, + truncatedCommitHash, + SPACE, + "(${BuildConfig.VERSION_CODE})" + ).joinToString(separator = "") + } + } + return "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + } + + override fun versionCode(): String = "${BuildConfig.VERSION_CODE}" + override fun model(): String = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.PRODUCT})" + override fun language(): String = Locale.getDefault().displayName + + @Composable + override fun inDarkTheme(): String = if (isSystemInDarkTheme()) DARK_THEME_ON else DARK_THEME_OFF + override fun nfcInformation(context: Context): String = "nicht vorhanden" +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt similarity index 77% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt index 8ef9b496..5eeee5bc 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/info/MockBuildConfigurationModule.kt @@ -16,12 +16,12 @@ * */ -package de.gematik.ti.erp.app.attestation.usecase +package de.gematik.ti.erp.app.info import org.kodein.di.DI -import org.kodein.di.bindProvider +import org.kodein.di.bind import org.kodein.di.instance -val integrityModule = DI.Module("integrityModule") { - bindProvider { IntegrityUseCase(instance()) } +val mockBuildConfigurationModule = DI.Module("mockBuildConfigurationModule") { + bind() with instance(MockBuildConfigInformation()) } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt similarity index 87% rename from app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt index 78db223f..b6024e1a 100644 --- a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyMockModule.kt @@ -18,12 +18,11 @@ package de.gematik.ti.erp.app.pharmacy -import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyLocalDataSource -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRemoteDataSource import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase @@ -33,15 +32,14 @@ import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance -val pharmacyMockModule = DI.Module("pharmacyTestModule") { +val pharmacyMockModule = DI.Module("pharmacyMockModule") { bindProvider { PharmacyRemoteDataSource(instance(), instance()) } - bindProvider { DefaultPharmacyLocalDataSource(instance()) } + bindProvider { PharmacyMockRepository() } bindProvider { ShippingContactRepository(instance(), instance()) } bindProvider { PharmacyDirectRedeemUseCase(instance()) } bindProvider { PharmacyMapsUseCase(instance(), instance(), instance()) } bindProvider { PharmacySearchUseCase(instance(), instance(), instance(), instance(), instance()) } bindProvider { PharmacyOverviewUseCase(instance(), instance()) } bindProvider { GetOrderStateUseCase(instance(), instance(), instance()) } - - bindProvider { PharmacyMockRepository() } + bindProvider { GetOverviewPharmaciesUseCase(instance()) } } diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt new file mode 100644 index 00000000..0707084a --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthority.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv + +import de.gematik.ti.erp.app.mock.BuildConfig + +class MockFileProviderAuthority : FileProviderAuthority { + override fun getFilePath(): String { + return "${BuildConfig.APPLICATION_ID}.fileprovider" + } +} diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt new file mode 100644 index 00000000..fbf165f1 --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pkv/MockFileProviderAuthorityModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +package de.gematik.ti.erp.app.pkv + +import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.instance + +val mockFileProviderAuthorityModule = DI.Module("mockFileProviderAuthorityModule") { + bind() with instance(MockFileProviderAuthority()) +} diff --git a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 5d5c99d6..c456ce03 100644 --- a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 5d5c99d6..c456ce03 100644 --- a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/android/build.gradle.kts b/app/android/build.gradle.kts index dedd7fa9..fec6660e 100644 --- a/app/android/build.gradle.kts +++ b/app/android/build.gradle.kts @@ -33,10 +33,6 @@ afterEvaluate { } } -tasks.named("preBuild") { - dependsOn(":ktlint", ":detekt") -} - licenseReport { generateCsvReport = false generateHtmlReport = false @@ -126,6 +122,17 @@ android { } } } + create("minifiedDebug") { + initWith(debug) + applicationIdSuffix = ".debug" + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + resValue("string", "app_label", "E-Rezept DebugRelease") + } } flavorDimensions += listOf("version") productFlavors { diff --git a/app/android/proguard-rules.pro b/app/android/proguard-rules.pro index d10b8ab7..0a860ee8 100644 --- a/app/android/proguard-rules.pro +++ b/app/android/proguard-rules.pro @@ -116,6 +116,8 @@ -keepclasseswithmembers class java.lang.reflect.** { *; } -keepclasseswithmembers class java.lang.Class +-keepclasseswithmembers class de.gematik.ti.erp.app.base.BaseActivity + # https://github.com/square/retrofit/issues/3751 # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). -keep,allowobfuscation,allowshrinking interface retrofit2.Call diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt new file mode 100644 index 00000000..1e1520a9 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/compose/EditableTextFieldTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.compose + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextReplacement +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.EditableTextField +import de.gematik.ti.erp.app.utils.compose.ErrorTextTag +import io.github.aakira.napier.Napier +import org.junit.Rule +import org.junit.Test + +class EditableTextFieldTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun testEditableTextField() { + val text = "some random text" + val newText = "some new text" + + composeTestRule.setContent { + AppTheme { + EditableTextField( + text = text, + textMinLength = 20, + onDoneClicked = { + Napier.d { "text is $it" } + } + ) + } + } + + // test existing text + composeTestRule.onNodeWithText(text).assertIsDisplayed() + composeTestRule.onNodeWithText(text).performClick() + composeTestRule.onNodeWithText(text).assertHasClickAction() + + // test text change + composeTestRule.onNodeWithText(text).performTextReplacement(newText) + composeTestRule.onNodeWithText(newText).assertIsDisplayed() + + // test showing error text + composeTestRule.onNodeWithText(newText).performTextClearance() + composeTestRule.onNodeWithTag(ErrorTextTag).assertIsDisplayed() + } +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt b/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt index f21c6cdf..80e72a1b 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt @@ -16,6 +16,8 @@ * */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app import androidx.lifecycle.ProcessLifecycleOwner @@ -23,7 +25,8 @@ import com.contentsquare.android.Contentsquare import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import de.gematik.ti.erp.app.di.appModules import de.gematik.ti.erp.app.di.featureModule -import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.userauthentication.observer.ProcessLifecycleObserver import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import org.kodein.di.DI @@ -38,11 +41,12 @@ class DefaultErezeptApp : ErezeptApp(), DIAware { import(androidXModule(this@DefaultErezeptApp)) importAll(appModules) importAll(featureModule, allowOverride = true) - bindSingleton { AuthenticationUseCase(instance()) } + bindSingleton { InactivityTimeoutObserver(instance(), instance()) } + bindSingleton { ProcessLifecycleObserver(ProcessLifecycleOwner, instance()) } bindSingleton { VisibleDebugTree() } } - private val authUseCase: AuthenticationUseCase by instance() + private val processLifecycleObserver: ProcessLifecycleObserver by instance() private val visibleDebugTree: VisibleDebugTree by instance() @@ -53,10 +57,10 @@ class DefaultErezeptApp : ErezeptApp(), DIAware { Napier.base(visibleDebugTree) } - ProcessLifecycleOwner.get().lifecycle.apply { - addObserver(authUseCase) - } + processLifecycleObserver.observeForInactivity() + PDFBoxResourceLoader.init(this) + Contentsquare.start(this) } } diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt b/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt index 3f653a7e..a4616382 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt @@ -76,7 +76,7 @@ val appModules = DI.Module("appModules") { bindProvider { AnalyticsUseCase(instance()) } - bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag), instance()) } + bindSingleton { Analytics(instance(), instance(), instance(), instance()) } importAll( buildConfigInformationModule, diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt index 99db0cbd..9f25f6ee 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt @@ -19,9 +19,6 @@ package de.gematik.ti.erp.app.di object ModuleNames { - const val cardWallModule = "cardWallModule" - const val integrityModule = "integrityModule" - const val networkModule = "networkModule" - const val pharmacyModule = "pharmacyModule" const val buildConfigInformationModule = "buildConfigInformationModule" + const val fileProviderAuthorityModule = "fileProviderAuthorityModule" } diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt index deca5d8a..3977bc00 100644 --- a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt @@ -18,10 +18,11 @@ package de.gematik.ti.erp.app.pkv +import de.gematik.ti.erp.app.di.ModuleNames import org.kodein.di.DI import org.kodein.di.bind import org.kodein.di.instance -val fileProviderAuthorityModule = DI.Module("fileProviderAuthorityModule") { +val fileProviderAuthorityModule = DI.Module(ModuleNames.fileProviderAuthorityModule) { bind() with instance(DefaultFileProviderAuthority()) } diff --git a/app/android/src/main/res/raw/analytics_identifier.json b/app/android/src/main/res/raw/analytics_identifier.json deleted file mode 100644 index 9518c43c..00000000 --- a/app/android/src/main/res/raw/analytics_identifier.json +++ /dev/null @@ -1,487 +0,0 @@ -[ - { - "main": { - "name": "main" - } - }, - { - "main_createProfile": { - "name": "main:createProfile" - } - }, - { - "main_editProfilePicture": { - "name": "main:editProfilePicture" - } - }, - { - "main_editName": { - "name": "main:editName" - } - }, - { - "main_scanner": { - "name": "main:scanner" - } - }, - { - "main_deviceSecurity": { - "name": "main:deviceSecurity" - } - }, - { - "main_integrityWarning": { - "name": "main:integrityWarning" - } - }, - { - "main_prescriptionArchive": { - "name": "main:prescriptionArchive" - } - }, - { - "main_welcomeDrawer": { - "name": "main:welcomeDrawer" - } - }, - { - "prescriptionDetail": { - "name": "prescriptionDetail" - } - }, - { - "prescriptionDetail_medication": { - "name": "prescriptionDetail:medication" - } - }, - { - "prescriptionDetail_patient": { - "name": "prescriptionDetail:patient" - } - }, - { - "prescriptionDetail_practitioner": { - "name": "prescriptionDetail:practitioner" - } - }, - { - "prescriptionDetail_organization": { - "name": "prescriptionDetail:organization" - } - }, - { - "prescriptionDetail_accidentInfo": { - "name": "prescriptionDetail:accidentInfo" - } - }, - { - "prescriptionDetail_technicalInfo": { - "name": "prescriptionDetail:technicalInfo" - } - }, - { - "prescriptionDetail_sharePrescription": { - "name": "prescriptionDetail:sharePrescription" - } - }, - { - "prescriptionDetail_directAssignmentInfo": { - "name": "prescriptionDetail:directAssignmentInfo" - } - }, - { - "prescriptionDetail_substitutionInfo": { - "name": "prescriptionDetail:substitutionInfo" - } - }, - { - "prescriptionDetail_errorInfo": { - "name": "prescriptionDetail:errorInfo" - } - }, - { - "prescriptionDetail_prescriptionValidityInfo": { - "name": "prescriptionDetail:prescriptionValidityInfo" - } - }, - { - "prescriptionDetail_scannedPrescriptionInfo": { - "name": "prescriptionDetail:scannedPrescriptionInfo" - } - }, - { - "prescriptionDetail_coPaymentInfo": { - "name": "prescriptionDetail:coPaymentInfo" - } - }, - { - "prescriptionDetail_emergencyServiceFeeInfo": { - "name": "prescriptionDetail:emergencyServiceFeeInfo" - } - }, - { - "prescriptionDetail_medicationOverview": { - "name": "prescriptionDetail:medicationOverview" - } - }, - { - "prescriptionDetail_medication_ingredients": { - "name": "prescriptionDetail:medication_ingredients" - } - }, - { - "redeem_methodSelection": { - "name": "redeem:methodSelection" - } - }, - { - "redeem_prescriptionAllOrSelection": { - "name": "redeem:prescriptionAllOrSelection" - } - }, - { - "redeem_prescriptionChooseSubset": { - "name": "redeem:prescriptionChooseSubset" - } - }, - { - "redeem_matrixCode": { - "name": "redeem:matrixCode" - } - }, - { - "redeem_viaAVS": { - "name": "redeem:viaAVS" - } - }, - { - "redeem_viaTI": { - "name": "redeem:viaTI" - } - }, - { - "redeem_success": { - "name": "redeem:success" - } - }, - { - "redeem_editContactInformation": { - "name": "redeem:editContactInformation" - } - }, - { - "pharmacySearch": { - "name": "pharmacySearch" - } - }, - { - "pharmacySearch_detail": { - "name": "pharmacySearch:detail" - } - }, - { - "pharmacySearch_filter": { - "name": "pharmacySearch:filter" - } - }, - { - "pharmacySearch_map": { - "name": "pharmacySearch:map" - } - }, - { - "pharmacySearch_selectedPharmacy": { - "name": "pharmacySearch:selectedPharmacy" - } - }, - { - "cardWall": { - "name": "cardWall" - } - }, - { - "cardWall_introduction": { - "name": "cardWall:welcome" - } - }, - { - "cardWall_notCapable": { - "name": "cardWall:notCapable" - } - }, - { - "cardWall_CAN": { - "name": "cardWall:CAN" - } - }, - { - "cardWall_PIN": { - "name": "cardWall:PIN" - } - }, - { - "cardWall_scanCAN": { - "name": "cardWall:scanCAN" - } - }, - { - "cardWall_saveLogin": { - "name": "cardWall:saveCredentials:initial" - } - }, - { - "cardWall_saveLoginSecurityInfo": { - "name": "cardWall:saveCredentials:information" - } - }, - { - "cardWall_readCard": { - "name": "cardWall:connect" - } - }, - { - "cardWall_extAuth": { - "name": "cardWall:extAuth" - } - }, - { - "cardWall_extAuthConfirm": { - "name": "cardWall:extAuthConfirm" - } - }, - { - "troubleShooting": { - "name": "troubleShooting" - } - }, - { - "troubleShooting_readCardHelp1": { - "name": "troubleShooting:readCardHelp1" - } - }, - { - "troubleShooting_readCardHelp2": { - "name": "troubleShooting:readCardHelp2" - } - }, - { - "troubleShooting_readCardHelp3": { - "name": "troubleShooting:readCardHelp3" - } - }, - { - "contactInsuranceCompany": { - "name": "contactInsuranceCompany" - } - }, - { - "contactInsuranceCompany_selectKK": { - "name": "contactInsuranceCompany:selectKK" - } - }, - { - "contactInsuranceCompany_selectReason": { - "name": "contactInsuranceCompany:selectReason" - } - }, - { - "contactInsuranceCompany_selectMethod": { - "name": "contactInsuranceCompany:selectMethod" - } - }, - { - "orders": { - "name": "orders" - } - }, - { - "orders_detail": { - "name": "orders:detail" - } - }, - { - "orders_pickupCode": { - "name": "orders:pickupCode" - } - }, - { - "alert": { - "name": "General Alert Dialog" - } - }, - { - "errorAlert": { - "name": "Error Alert" - } - }, - { - "healthCardPassword_forgotPin": { - "name": "healthCardPassword:forgotPin" - } - }, - { - "healthCardPassword_setCustomPin": { - "name": "healthCardPassword:setCustomPin" - } - }, - { - "healthCardPassword_unlockCard": { - "name": "healthCardPassword:unlockCard" - } - }, - { - "healthCardPassword_introduction": { - "name": "healthCardPassword:introduction" - } - }, - { - "healthCardPassword_can": { - "name": "healthCardPassword:can" - } - }, - { - "healthCardPassword_puk": { - "name": "healthCardPassword:puk" - } - }, - { - "healthCardPassword_oldPin": { - "name": "healthCardPassword:oldPin" - } - }, - { - "healthCardPassword_pin": { - "name": "healthCardPassword:pin" - } - }, - { - "healthCardPassword_readCard": { - "name": "healthCardPassword:readCard" - } - }, - { - "healthCardPassword_scanner": { - "name": "healthCardPassword:scanner" - } - }, - { - "settings": { - "name": "settings" - } - }, - { - "settings_accessibility": { - "name": "settings:accessibility" - } - }, - { - "settings_authenticationMethods": { - "name": "settings:authenticationMethods" - } - }, - { - "settings_authenticationMethods_setAppPassword": { - "name": "settings:authenticationMethods:setAppPassword" - } - }, - { - "settings_productImprovements": { - "name": "settings:productImprovements" - } - }, - { - "settings_productImprovements_complyTracking": { - "name": "settings:productImprovements:complyTracking" - } - }, - { - "settings_legalNotice": { - "name": "settings:legalNotice" - } - }, - { - "settings_dataProtection": { - "name": "settings:dataProtection" - } - }, - { - "settings_openSourceLicence": { - "name": "settings:openSourceLicence" - } - }, - { - "settings_additionalLicence": { - "name": "settings:additionalLicence" - } - }, - { - "settings_termsOfUse": { - "name": "settings:termsOfUse" - } - }, - { - "profile": { - "name": "profile" - } - }, - { - "profile_editPicture": { - "name": "profile:editPicture" - } - }, - { - "profile_editPicture_imageCropper": { - "name": "profile:editPicture:imageCropper" - } - }, - { - "settings_newProfile": { - "name": "settings:newProfile" - } - }, - { - "profile_token": { - "name": "profile:token" - } - }, - { - "profile_registeredDevices": { - "name": "profile:registeredDevices" - } - }, - { - "profile_auditEvents": { - "name": "profile:auditEvents" - } - }, - { - "chargeItem_list": { - "name": "chargeItem:list" - } - }, - { - "chargeItem_details": { - "name": "chargeItem:details" - } - }, - { - "chargeItem_details_expanded": { - "name": "chargeItem:details:expanded" - } - }, - { - "chargeItem_share": { - "name": "chargeItem:share" - } - }, - { - "mlKit": { - "name": "mlKit" - } - }, - { - "mlKit_information": { - "name": "mlKit:information" - } - } -] diff --git a/app/android/src/main/res/raw/animation_courier.webm b/app/android/src/main/res/raw/animation_courier.webm deleted file mode 100644 index 449c50b1..00000000 Binary files a/app/android/src/main/res/raw/animation_courier.webm and /dev/null differ diff --git a/app/android/src/main/res/raw/animation_local.webm b/app/android/src/main/res/raw/animation_local.webm deleted file mode 100644 index 3e1f93e3..00000000 Binary files a/app/android/src/main/res/raw/animation_local.webm and /dev/null differ diff --git a/app/android/src/main/res/raw/animation_mail.webm b/app/android/src/main/res/raw/animation_mail.webm deleted file mode 100644 index 871ffac0..00000000 Binary files a/app/android/src/main/res/raw/animation_mail.webm and /dev/null differ diff --git a/app/android/src/main/res/raw/animation_pulse_lottie.json b/app/android/src/main/res/raw/animation_pulse_lottie.json deleted file mode 100644 index 80cf57e7..00000000 --- a/app/android/src/main/res/raw/animation_pulse_lottie.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.1.16","fr":30,"ip":0,"op":60,"w":360,"h":360,"nm":"Pre-comp 2","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":16,"s":[0],"e":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":46,"s":[40],"e":[0]},{"t":76.0000030955435}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.309,99.021,0],"ix":2},"a":{"a":0,"k":[0.309,-0.979,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":16,"s":[0,0,100],"e":[100,100,100]},{"t":76.0000030955435}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[148.156,148.156],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2627450980392157,0.6,0.8823529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.309,-0.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":16.0000006516934,"op":76.0000030955435,"st":16.0000006516934,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":30,"s":[40],"e":[0]},{"t":60.0000024438501}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.309,99.021,0],"ix":2},"a":{"a":0,"k":[0.309,-0.979,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"t":60.0000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[148.156,148.156],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2627450980392157,0.6,0.8823529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.309,-0.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":76.0000030955435,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180,180,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":39.0000015885026,"st":-37.0000015070409,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180,180,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":23.0000009368092,"op":60.0000024438501,"st":23.0000009368092,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/android/src/main/res/raw/device_lottie.json b/app/android/src/main/res/raw/device_lottie.json deleted file mode 100644 index 8b9f49fb..00000000 --- a/app/android/src/main/res/raw/device_lottie.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.6.6","ip":0,"op":1,"fr":60,"w":241,"h":146,"layers":[{"ind":1899,"nm":"surface8209","ao":0,"ip":0,"op":60,"st":0,"ty":4,"ks":{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[133.33,133.33]},"sk":{"k":0},"sa":{"k":0}},"shapes":[{"ty":"gr","hd":false,"nm":"surface8209","it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.05,0.06],[0,0.08],[-0.05,0.06],[-0.07,0.02],[-0.21,0],[-0.19,-0.1],[-0.05,-0.07],[0,-0.08],[0.05,-0.07],[0.08,-0.02],[0.21,0],[0.19,0.1]],"o":[[-0.07,-0.02],[-0.05,-0.07],[0,-0.08],[0.05,-0.07],[0.19,-0.1],[0.21,0],[0.08,0.02],[0.05,0.06],[0,0.08],[-0.05,0.06],[-0.19,0.1],[-0.21,0],[0,0]],"v":[[149.08,18.38],[148.89,18.25],[148.82,18.03],[148.89,17.81],[149.08,17.67],[149.7,17.52],[150.31,17.67],[150.5,17.81],[150.57,18.03],[150.5,18.25],[150.31,18.38],[149.7,18.53],[149.08,18.38]],"c":true}}},{"ty":"fl","o":{"k":10},"c":{"k":[0.26,0.6,0.88,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0.66],[1.15,0],[0,-0.66],[-1.15,0]],"o":[[1.15,0],[0,-0.66],[-1.15,0],[0,0.66],[0,0]],"v":[[149.7,19.23],[151.78,18.03],[149.7,16.82],[147.62,18.03],[149.7,19.23]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.75,0.89,0.97,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[2.75,1.16],[0.27,0.15],[0,0],[0.41,0.55],[-2.07,1.19],[0,0],[-2.75,-1.59],[0,0],[0.01,-0.01],[-2.14,-1.23],[-2.13,1.23],[0,0],[0,0],[2.75,-1.59]],"o":[[0,0],[-2.48,1.43],[-0.29,-0.12],[0,0],[-0.61,-0.32],[-1.04,-1.47],[0,0],[2.75,-1.59],[0,0],[-0.02,0],[-2.14,1.23],[2.14,1.23],[0,0],[0,0],[2.75,1.59],[0,0]],"v":[[172.89,35.74],[63.87,98.71],[54.75,99.12],[53.92,98.71],[10,73.32],[8.45,71.99],[10,67.58],[119.02,4.64],[128.97,4.64],[147.06,15.09],[147.02,15.11],[147.02,19.57],[154.76,19.57],[154.8,19.54],[172.89,29.99],[172.89,35.74]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.26,0.26,0.26,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0],[2.14,1.23],[-2.14,1.23],[0,0],[0,0],[2.75,-1.59],[0,0],[-2.75,-1.58],[0,0],[-2.75,1.59],[0,0],[2.75,1.59]],"o":[[0,0],[0,0],[-2.14,1.23],[-2.14,-1.23],[0,0],[0,0],[-2.75,-1.59],[0,0],[-2.75,1.59],[0,0],[2.75,1.59],[0,0],[2.75,-1.58],[0,0]],"v":[[172.89,30],[154.8,19.55],[154.75,19.57],[147.02,19.57],[147.02,15.11],[147.06,15.07],[128.97,4.62],[119.02,4.62],[9.99,67.58],[9.99,73.32],[53.91,98.69],[63.86,98.69],[172.89,35.74],[172.89,30]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[1,1,1,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.27],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.61],[0,0],[3.93,2.27],[0,0],[-4.54,2.62],[0,0]],"v":[[49.51,103.04],[3.17,76.27],[4.29,67.43],[116.21,2.8],[131.52,2.16],[177.87,28.93],[176.75,37.77],[64.83,102.4],[49.51,103.04]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0.5],[-0.11,0.21],[-0.2,0.14],[0,0],[0,-0.5],[0.11,-0.21],[0.2,-0.14]],"o":[[0,0],[-0.37,0.21],[0,-0.24],[0.11,-0.22],[0,0],[0.37,-0.21],[0,0.24],[-0.11,0.21],[0,0]],"v":[[157.77,52.09],[152.07,55.38],[151.41,54.88],[151.59,54.18],[152.07,53.64],[157.77,50.35],[158.41,50.84],[158.24,51.54],[157.77,52.09]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[1,0.7,0.7,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0.49],[-0.11,0.22],[-0.2,0.14],[0,0],[0,-0.5],[0.11,-0.21],[0.2,-0.14]],"o":[[0,0],[-0.37,0.21],[0,-0.25],[0.11,-0.21],[0,0],[0.38,-0.21],[0,0.24],[-0.11,0.22],[0,0]],"v":[[166.49,47.04],[160.79,50.32],[160.14,49.83],[160.32,49.13],[160.79,48.58],[166.49,45.29],[167.14,45.79],[166.97,46.49],[166.49,47.04]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.96,0.96,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0.12,0.23],[0,0.26],[-0.39,-0.21],[0,0],[-0.12,-0.23],[0,-0.26],[0.39,0.22]],"o":[[0,0],[-0.21,-0.15],[-0.12,-0.23],[0,-0.52],[0,0],[0.21,0.15],[0.12,0.23],[0,0.52],[0,0]],"v":[[27.67,93.77],[21.54,90.23],[21.03,89.64],[20.84,88.89],[21.54,88.36],[27.67,91.9],[28.18,92.48],[28.37,93.24],[27.67,93.77]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[41.3,101.21],[41.3,100.21],[40.44,99.72],[40.44,100.71],[41.3,101.21]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[39.07,99.91],[39.07,98.92],[38.21,98.43],[38.21,99.42],[39.07,99.91]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[36.84,98.63],[36.84,97.64],[35.98,97.14],[35.98,98.14],[36.84,98.63]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[34.61,97.34],[34.61,96.34],[33.75,95.85],[33.75,96.84],[34.61,97.34]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[15.45,86.29],[15.46,85.29],[14.6,84.8],[14.59,85.79],[15.45,86.29]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[13.22,84.99],[13.23,84],[12.37,83.5],[12.36,84.5],[13.22,84.99]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[10.99,83.71],[11,82.71],[10.13,82.22],[10.13,83.21],[10.99,83.71]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[8.76,82.41],[8.77,81.42],[7.9,80.93],[7.9,81.92],[8.76,82.41]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.26],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.6],[0,0],[3.93,2.27],[0,0],[-4.54,2.61],[0,0]],"v":[[49.51,103.69],[3.17,76.93],[4.29,68.08],[116.21,3.46],[131.52,2.83],[177.87,29.59],[176.75,38.43],[64.83,103.06],[49.51,103.69]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.26,0.26,0.26,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[0.52,76.65],[0.52,72.39],[5.52,74.27]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.27],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.6],[0,0],[3.93,2.27],[0,0],[-4.54,2.62],[0,0]],"v":[[49.51,107.3],[3.17,80.54],[4.29,71.7],[116.21,7.08],[131.52,6.44],[177.87,33.21],[176.75,42.05],[64.83,106.67],[49.51,107.3]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[180.52,36.89],[180.52,32.79],[176.54,35.35]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]}]}],"meta":{"g":"LF SVG to Lottie"}} \ No newline at end of file diff --git a/app/android/src/main/res/raw/health_insurance_contacts.json b/app/android/src/main/res/raw/health_insurance_contacts.json deleted file mode 100644 index 4913e9f6..00000000 --- a/app/android/src/main/res/raw/health_insurance_contacts.json +++ /dev/null @@ -1,1157 +0,0 @@ -[ - { - "name": "AOK Baden-Württemberg", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/elektronische-gesundheitskarte-egk/", - "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Bayern", - "healthCardAndPinPhone": "+498922844050", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Bremen", - "healthCardAndPinPhone": "+4942117610", - "healthCardAndPinMail": "info@hb.aok.de", - "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "AOK Bremerhaven", - "healthCardAndPinPhone": "+49471160", - "healthCardAndPinMail": "info@hb.aok.de", - "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "AOK - Die Gesundheitskasse Hessen", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/?reg=hessen", - "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/?reg=hessen", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK - Die Gesundheitskasse Niedersachsen", - "healthCardAndPinPhone": "+498000265637", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Nordost", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "eGK_online@nordost.aok.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Nordwest - Die Gesundheitskasse", - "healthCardAndPinPhone": "+498002655060", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK PLUS - Die Gesundheitskasse für Sachsen und Thüringen", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.aok.de/pk/plus/inhalt/elektronische-gesundheitskarte-anfordern/", - "pinUrl": "https://www.aok.de/pk/plus/inhalt/elektronische-gesundheitskarte-11/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Rheinland/Hamburg - Die Gesundheitskasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "aok@rh.aok.de", - "healthCardAndPinUrl": null, - "pinUrl": "https://www.aok.de/pk/rh/inhalt/pin-zur-elektronischen-gesundheitskarte-egk-5/", - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Rheinland-Pfalz/Saarland - Die Gesundheitskasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.aok.de/pk/rps/inhalt/die-haeufigsten-fragen-und-antworten-zum-e-rezept-4/", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "AOK Sachsen-Anhalt - Die Gesundheitskasse", - "healthCardAndPinPhone": "+4908002265725", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Audi BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.audibkk.de/e-rezept-gematik-egkpin/", - "pinUrl": "https://www.audibkk.de/e-rezept-gematik-pin/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BAHN-BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bahn-bkk.de/egk-erezept", - "pinUrl": "https://www.bahn-bkk.de/egk-erezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Deutsche Bank AG", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkkdb.de/leistungen-beratung/alle-leistungen/alle-leistungen-von-a-z/versichertenkarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BARMER", - "healthCardAndPinPhone": "+498003331010", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.barmer.de/gematik-eRezept", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "DIE BERGISCHE KRANKENKASSE", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bergische-krankenkasse.de/digital/e-rezept", - "pinUrl": "https://www.bergische-krankenkasse.de/digital/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Bertelsmann BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bertelsmann-bkk.de/erezept-egk-pin", - "pinUrl": "https://www.bertelsmann-bkk.de/erezept-egk-pin", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BIG direkt gesund", - "healthCardAndPinPhone": "+4980054565456", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.big-direkt.de/de/erezept-der-gematik-nutzen", - "pinUrl": "https://www.big-direkt.de/de/erezept-der-gematik-nutzen", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Akzo Nobel Bayern", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-akzo.de/service/elektronische-gesundheitskarte-egk", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK B. Braun Aesculap", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-bba.de/egk-pin-bestellen", - "pinUrl": "https://www.bkk-bba.de/pin-bestellen", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK BPW Bergische Achsen KG", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK EUREGIO", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-euregio.de/elektronische-gesundheitskarte", - "pinUrl": "https://www.bkk-euregio.de/elektronische-gesundheitskarte", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK EVM", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK EWE", - "healthCardAndPinPhone": "+49441350285108", - "healthCardAndPinMail": "versicherung@bkk-ewe.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Diakonie", - "healthCardAndPinPhone": "+49521329876120", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-diakonie.de/elektronische-gesundheitskarte/egk-bestellen/", - "pinUrl": "https://www.bkk-diakonie.de/elektronische-gesundheitskarte/egk-bestellen/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK exklusiv", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://bkkexklusiv.de/gesundheitskarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Faber-Castell & Partner", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "erezept@bkk-faber-castell.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK firmus", - "healthCardAndPinPhone": "+4942164343", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-firmus.de/beratung-und-service/online-tools/e-rezept.html", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Freudenberg", - "healthCardAndPinPhone": "+4962016905001", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK GILDEMEISTER SEIDENSTICKER", - "healthCardAndPinPhone": "+498000255255", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkkgs.de/e-rezept", - "pinUrl": "https://www.bkkgs.de/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK GRILLO-WERKE AG", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Groz-Beckert", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "info@bkk-gb.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Herford Minden Ravensberg", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Herkules", - "healthCardAndPinPhone": "+49561208550", - "healthCardAndPinMail": "info@bkk-herkules.de", - "healthCardAndPinUrl": "https://www.bkk-herkules.de/service/gesundheitskarte-und-lichtbild/", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK HMR", - "healthCardAndPinPhone": "+4952211026210", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK KARL MAYER", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Linde", - "healthCardAndPinPhone": "+496117366781", - "healthCardAndPinMail": "egk@bkk-linde.de", - "healthCardAndPinUrl": "https://bkkln.de/epa-egkpin", - "pinUrl": "https://bkkln.de/epa-egkpin", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Melitta HMR", - "healthCardAndPinPhone": "+4957197590", - "healthCardAndPinMail": "info@bkk-melitta.de", - "healthCardAndPinUrl": "https://www.bkk-melitta.de/", - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK MAHLE", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-mahle.de/service/elektronische-gesundheitskarte-egk", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Miele", - "healthCardAndPinPhone": "+498008002189", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.miele-bkk.de/service/elektronische-gesundheitskarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK MTU", - "healthCardAndPinPhone": "+497541907100", - "healthCardAndPinMail": "info@bkk-mtu.de", - "healthCardAndPinUrl": "https://www.bkk-mtu.de/unsere-leistungen/leistungen-a-z/elektronische-gesundheitskarte-egk-bkk-mtu-service/", - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK PFAFF", - "healthCardAndPinPhone": "+49631318760", - "healthCardAndPinMail": "info@bkk-pfaff.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Pfalz", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkkpfalz.de/service-informationen/elektronische-gesundheitskarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK ProVita", - "healthCardAndPinPhone": "+498006648808", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://bkk-provita.de/service-info/e-rezept/", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Public", - "healthCardAndPinPhone": "+495341405600", - "healthCardAndPinMail": "service@bkk-public.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK PricewaterhouseCoopers", - "healthCardAndPinPhone": "+498002557920", - "healthCardAndPinMail": "erezept@bkk-pwc.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK Rieker RICOSTA Weisser", - "healthCardAndPinPhone": "+4974625793030", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK RWE", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkkrwe.de/e-rezept", - "pinUrl": "https://www.bkkrwe.de/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Salzgitter", - "healthCardAndPinPhone": "+495341405700", - "healthCardAndPinMail": "service@bkk-salzgitter.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK SBH", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-sbh.de/e-rezept/", - "pinUrl": "https://bkk-sbh.de/e-rezept/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Scheufelen", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-scheufelen.de/e-rezept", - "pinUrl": "https://www.bkk-scheufelen.de/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Schwarzwald-BaarHeuberg", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK STADT AUGSBURG", - "healthCardAndPinPhone": "+498213243231", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Technoform", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-technoform.de/index.php?p=page&ID=11", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Textilgruppe Hof", - "healthCardAndPinPhone": "+498002558440", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Verkehrsbau Union (VBU)", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "info@bkk-vbu.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK VDN", - "healthCardAndPinPhone": "+49230498260", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK VerbundPlus", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-verbundplus.de/ihre-mitgliedschaft/elektronische-gesundheitskarte/", - "pinUrl": "https://www.bkk-verbundplus.de/nfc-karte-pin", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Voralb", - "healthCardAndPinPhone": "+4970229324639", - "healthCardAndPinMail": "beitraege@bkk-voralb.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK Werra-Meissner", - "healthCardAndPinPhone": "+490565174510", - "healthCardAndPinMail": "info@bkk-wm.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK Wirtschaft & Finanzen", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk-wf.de/e-rezept/", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK Würth", - "healthCardAndPinPhone": "+49794091900", - "healthCardAndPinMail": "info@bkk-wuerth.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "BKK ZF & Partner", - "healthCardAndPinPhone": "+493381306652512", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK_DürkoppAdler", - "healthCardAndPinPhone": "+495215578470", - "healthCardAndPinMail": "eRezept@bkk-da.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BKK24", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bkk24.de/e-rezept", - "pinUrl": "https://bkk24.de/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "BMW BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bmwbkk.de/egk", - "pinUrl": "https://www.bmwbkk.de/egk-pin-puk", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Bosch BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "info@bosch-bkk.de", - "healthCardAndPinUrl": "https://meine.bosch-bkk.de/bitgo_gs/de/oeffentlich/login/login.xhtml", - "pinUrl": "https://meine.bosch-bkk.de/bitgo_gs/de/oeffentlich/login/login.xhtml", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Continentale BKK", - "healthCardAndPinPhone": "+498006262626", - "healthCardAndPinMail": "kundenservice@continentale-bkk.de", - "healthCardAndPinUrl": "https://www.continentale-bkk.de/kontakt/kontaktformular/", - "pinUrl": "https://www.continentale-bkk.de/kontakt/kontaktformular/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Mercedes-Benz BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.mercedes-benz-bkk.com/service/erezept", - "pinUrl": "https://www.mercedes-benz-bkk.com/service/erezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "DAK-Gesundheit", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.dak.de/dak/ihr-anliegen/elektronische-gesundheitskarte-2083662.html#/", - "pinUrl": "https://www.dak.de/dak/ihr-anliegen/elektronische-gesundheitskarte-2083662.html#/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Debeka BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": "https://www.debeka-bkk.de/erezept/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "energie - Betriebskrankenkasse", - "healthCardAndPinPhone": "+4951191110911", - "healthCardAndPinMail": "info@energie-bkk.de", - "healthCardAndPinUrl": "https://www.energie-bkk.de/das-erezept-8979.html", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Ernst & Young BKK", - "healthCardAndPinPhone": "+495661707670", - "healthCardAndPinMail": "versicherung@ey-bkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "Heimat Krankenkasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.heimat-krankenkasse.de/egk-anfordern", - "pinUrl": "https://www.heimat-krankenkasse.de/egk-pin-anfordern", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "HEK - Hanseatische Krankenkasse", - "healthCardAndPinPhone": "+498000213213", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": "https://www.hek.de/egk", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Handelskrankenkasse (hkk)", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.hkk.de/versicherung-und-tarife/allgemeine-infos/erezept-app", - "pinUrl": "https://www.hkk.de/versicherung-und-tarife/allgemeine-infos/erezept-app", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "IKK - Die Innovationskasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.die-ik.de/e-rezept", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "IKK Brandenburg und Berlin", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.ikkbb.de/erezept/auth-egk", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "IKK classic", - "healthCardAndPinPhone": "+498004551111", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "IKK gesund plus", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "IKK Südwest", - "healthCardAndPinPhone": "+498000119119", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.ikk-suedwest.de/service/persoenlicher-kundenberater/", - "pinUrl": "https://www.ikk-suedwest.de/service/persoenlicher-kundenberater/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Kaufmännische Krankenkasse - KKH", - "healthCardAndPinPhone": "+498005548640554", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "KNAPPSCHAFT", - "healthCardAndPinPhone": "+498000200501", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Koenig & Bauer BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "erezept@koenig-bauer-bkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "Krones BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "Merck BKK", - "healthCardAndPinPhone": "+496151722256", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "mhplus Betriebskrankenkasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.mhplus-krankenkasse.de/privatkunden/unser-service/services-fuer-mitglieder/mhplus-gesundheitskarte-bestellen", - "pinUrl": "https://www.mhplus-krankenkasse.de/privatkunden/unser-service/gesundheit-digital/elektronische-patientenakte/registrierung-fuer-digitale-anwendungen", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Mobil Krankenkasse", - "healthCardAndPinPhone": "+498002550800", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://mobil-krankenkasse.de/unser-service/elektronische-gesundheitskarte/pin-puk-egk.html", - "pinUrl": "https://mobil-krankenkasse.de/unser-service/elektronische-gesundheitskarte/pin-puk-egk.html", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Novitas BKK", - "healthCardAndPinPhone": "+498006566300", - "healthCardAndPinMail": "gesundheitskarte@novitas-bkk.de", - "healthCardAndPinUrl": "https://www.novitas-bkk.de/egk", - "pinUrl": "https://www.novitas-bkk.de/egk", - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "pronova BKK", - "healthCardAndPinPhone": "+49621533911000", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.pronovabkk.de/leistungen/elektronische-gesundheitskarte", - "pinUrl": "https://meine.pronovabkk.de/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "R+V Betriebskrankenkasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.ruv-bkk.de/leistungen/alle-leistungen-im-ueberblick/leistungen-a-z/e/erezept/", - "pinUrl": "https://www.ruv-bkk.de/leistungen/alle-leistungen-im-ueberblick/leistungen-a-z/e/erezept/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Salus BKK", - "healthCardAndPinPhone": "+498002213222", - "healthCardAndPinMail": "egk@salus-bkk.de", - "healthCardAndPinUrl": "https://www.salus-bkk.de/service-formulare/infos-zur-mitgliedschaft/meine-gesundheitskarte/elektronische-gesundheitskarte/", - "pinUrl": "https://www.salus-bkk.de/service-formulare/infos-zur-mitgliedschaft/meine-gesundheitskarte/elektronische-gesundheitskarte/", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "SIEMAG BKK", - "healthCardAndPinPhone": "+492733292929", - "healthCardAndPinMail": "info@siemagbkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Siemens-Betriebskrankenkasse (SBK)", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://meine.sbk.org/pin_gesundheitskarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "SECURVITA BKK", - "healthCardAndPinPhone": "+494033477", - "healthCardAndPinMail": "egk@securvita-bkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "SKD BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.skd-bkk.de/leistungen/26-elektronische-gesundheitskarte-egk/", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Südzucker BKK", - "healthCardAndPinPhone": "+496213285845", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Sozialversicherung für Landwirtschaft, Forsten und Gartenbau (SVLFG)", - "healthCardAndPinPhone": "+495617850", - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://portal.svlfg.de/svlfg-apps/gesundheitskarte", - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "Techniker Krankenkasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.tk.de/techniker/2113848", - "pinUrl": "https://www.tk.de/techniker/2113852", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "TUI BKK", - "healthCardAndPinPhone": "+495341405800", - "healthCardAndPinMail": "service@tui-bkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "VIACTIV BKK", - "healthCardAndPinPhone": "+498002221211", - "healthCardAndPinMail": "service@viactiv.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "vivida bkk", - "healthCardAndPinPhone": "+49800375537555", - "healthCardAndPinMail": "kundencenter@vividabkk.de", - "healthCardAndPinUrl": "https://www.vividabkk.de/de/service/e-rezept-info", - "pinUrl": "https://www.vividabkk.de/de/service/e-rezept-info", - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, - { - "name": "Wieland BKK", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.wieland-bkk.de/service/unsere-digitalen-moeglichkeiten/e-rezept", - "pinUrl": "https://www.wieland-bkk.de/service/unsere-digitalen-moeglichkeiten/e-rezept", - "subjectCardAndPinMail": null, - "bodyCardAndPinMail": null, - "subjectPinMail": null, - "bodyPinMail": null - }, - { - "name": "WMF Betriebskrankenkasse", - "healthCardAndPinPhone": null, - "healthCardAndPinMail": "service@wmf-bkk.de", - "healthCardAndPinUrl": null, - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - } -] \ No newline at end of file diff --git a/app/android/src/main/res/raw/healthcard_lottie.json b/app/android/src/main/res/raw/healthcard_lottie.json deleted file mode 100644 index f4391347..00000000 --- a/app/android/src/main/res/raw/healthcard_lottie.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.6.6","ip":0,"op":1,"fr":60,"w":172,"h":168,"layers":[{"ind":1426,"nm":"surface4347","ao":0,"ip":0,"op":60,"st":0,"ty":4,"ks":{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[133.33,133.33]},"sk":{"k":0},"sa":{"k":0}},"shapes":[{"ty":"gr","hd":false,"nm":"surface4347","it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[-1.88,-1.02],[0,0],[1.99,-1.2],[0,0],[1.89,1.09],[0,0],[-1.94,1.19]],"o":[[1.82,-1.12],[0,0],[2.05,1.11],[0,0],[-1.88,1.12],[0,0],[-1.96,-1.14],[0,0]],"v":[[48.59,29.29],[54.58,29.13],[121.92,65.54],[122.04,70.75],[76.98,97.8],[70.88,97.85],[6.3,60.38],[6.23,55.23]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.56,0.8,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[-0.94,-0.51],[0,0],[1.99,-1.2],[0,0],[1.89,1.09],[0,0],[-1.94,1.19]],"o":[[0.91,-0.56],[0,0],[2.05,1.11],[0,0],[-1.87,1.12],[0,0],[-1.96,-1.14],[0,0]],"v":[[50.07,29.14],[53.06,29.05],[121.92,66.29],[122.04,71.5],[76.98,98.55],[70.88,98.6],[6.3,61.13],[6.24,55.98]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.96,0.96,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]}]}],"meta":{"g":"LF SVG to Lottie"}} \ No newline at end of file diff --git a/app/android/src/main/res/raw/nfc_positions.json b/app/android/src/main/res/raw/nfc_positions.json deleted file mode 100644 index 1cc87b42..00000000 --- a/app/android/src/main/res/raw/nfc_positions.json +++ /dev/null @@ -1,1713 +0,0 @@ -[ - { - "manufacturer": "Huawei", - "marketingName": "HONOR 10", - "modelNames": [ - "COL-AL00", - "COL-AL10", - "COL-L29", - "COL-TL10" - ], - "nfcPos": { - "x0": 0.07851239669421484, - "y0": 0.011210762331838564, - "x1": 0.5165289256198347, - "y1": 0.09865470852017937 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR 20", - "modelNames": [ - "YAL-AL00", - "YAL-L21", - "YAL-TL00" - ], - "nfcPos": { - "x0": 0.265625, - "y0": 0.014084507042253521, - "x1": 0.7135416666666667, - "y1": 0.15023474178403756 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR 9", - "modelNames": [ - "STF-AL00", - "STF-AL10", - "STF-L09", - "STF-L09S", - "STF-TL10" - ], - "nfcPos": { - "x0": 0.0, - "y0": 0.0, - "x1": 0.4530386740331491, - "y1": 0.08735632183908046 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR Magic 2", - "modelNames": [ - "TNY-AL00", - "TNY-TL00" - ], - "nfcPos": { - "x0": 0.0, - "y0": 0.0, - "x1": 0.6772486772486772, - "y1": 0.10304449648711944 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR Note10", - "modelNames": [ - "RVL-AL09" - ], - "nfcPos": { - "x0": 0.17803030303030298, - "y0": 0.0044943820224719105, - "x1": 0.7992424242424243, - "y1": 0.056179775280898875 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR Play", - "modelNames": [ - "COR-AL00", - "COR-AL10", - "COR-L29", - "COR-TL10" - ], - "nfcPos": { - "x0": 0.042857142857142816, - "y0": 0.020179372197309416, - "x1": 0.6761904761904762, - "y1": 0.12556053811659193 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR V10", - "modelNames": [ - "BKL-AL00", - "BKL-AL20", - "BKL-TL10" - ], - "nfcPos": { - "x0": 0.07851239669421484, - "y0": 0.011210762331838564, - "x1": 0.5165289256198347, - "y1": 0.09865470852017937 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR V20", - "modelNames": [ - "PCT-TL10" - ], - "nfcPos": { - "x0": 0.3121693121693122, - "y0": 0.07621247113163972, - "x1": 0.5873015873015873, - "y1": 0.19630484988452657 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HONOR V9", - "modelNames": [ - "DUK-AL20", - "DUK-TL30" - ], - "nfcPos": { - "x0": 0.0, - "y0": 0.0, - "x1": 0.4530386740331491, - "y1": 0.08735632183908046 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate 10-Serie", - "modelNames": [ - "ALP-AL00", - "ALP-L09", - "ALP-L29", - "ALP-TL00", - "BLA-A09", - "BLA-AL00", - "BLA-L09", - "BLA-L29", - "BLA-TL00", - "RNE-L01", - "RNE-L03", - "RNE-L21", - "RNE-L23" - ], - "nfcPos": { - "x0": 0.1701030927835051, - "y0": 0.0, - "x1": 0.7731958762886598, - "y1": 0.12413793103448276 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate 20-Serie", - "modelNames": [ - "HMA-L09", - "HMA-L29", - "LYA-L0C", - "LYA-L29", - "EVR-AN00", - "EVR-N29", - "SNE-LX1", - "EVR-L29", - "EVR-TL00", - "EVR-N29", - "EVR-AL00", - "HMA-AL00", - "HMA-L09", - "HMA-L29", - "HMA-TL00", - "LYA-AL00", - "LYA-AL10", - "LYA-L09", - "LYA-L29", - "LYA-TL00", - "LYA-AL00P", - "SNE-LX1", - "SNE-LX2", - "SNE-LX3" - ], - "nfcPos": { - "x0": 0.2328042328042328, - "y0": 0.10161662817551963, - "x1": 0.7671957671957672, - "y1": 0.2678983833718245 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate 30-Serie", - "modelNames": [ - "TAS-L09", - "TAS-L29", - "TAS-AL00", - "TAS-TL00", - "LIO-L09", - "LIO-L29", - "LIO-AL00", - "LIO-TL00", - "LIO-N29", - "LIO-AL10", - "LIO-TL10", - "TAS-AN00", - "TAS-TN00", - "LIO-AN00m", - "SPL-AL00", - "SPL-TL00", - "LIO-N29", - "LIO-AN00P", - "LIO-AN00" - ], - "nfcPos": { - "x0": 0.12903225806451613, - "y0": 0.020833333333333332, - "x1": 0.8333333333333334, - "y1": 0.3055555555555556 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate 40-Serie", - "modelNames": [ - "NOH-NX9", - "NOH-AN00", - "NOH-AN01", - "NOP-AN00", - "OCE-AN10", - "NOH-AL00", - "OCE-AN50", - "NOP-AN00", - "OCE-AL50" - ], - "nfcPos": { - "x0": 0.08860759493670889, - "y0": 0.012587412587412588, - "x1": 0.9113924050632911, - "y1": 0.35664335664335667 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate 9-Serie", - "modelNames": [ - "BLL-L23", - "HUAWEI BLL-L23", - "MHA-AL00", - "MHA-L09", - "MHA-L29", - "MHA-TL00", - "LON-AL00", - "LON-L29" - ], - "nfcPos": { - "x0": 0.17431192660550454, - "y0": 0.0022935779816513763, - "x1": 0.8027522935779816, - "y1": 0.06422018348623854 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate RS", - "modelNames": [ - "NEO-AL00", - "NEO-L29" - ], - "nfcPos": { - "x0": 0.13440860215053763, - "y0": 0.02947845804988662, - "x1": 0.8763440860215054, - "y1": 0.1836734693877551 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI Mate X-Serie", - "modelNames": [ - "TAH-AN00", - "TAH-N29m" - ], - "nfcPos": { - "x0": 0.023400936037441533, - "y0": 0.0, - "x1": 0.37597503900156004, - "y1": 0.04741980474198047 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI nova 2s", - "modelNames": [ - "HWI-AL00", - "HWI-TL00" - ], - "nfcPos": { - "x0": 0.07106598984771573, - "y0": 0.0022988505747126436, - "x1": 0.5076142131979695, - "y1": 0.12413793103448276 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI nova 3-Serie", - "modelNames": [ - "INE-LX1", - "INE-LX2r", - "PAR-AL00", - "PAR-L21", - "PAR-L29", - "PAR-LX1", - "PAR-LX1M", - "PAR-LX9", - "PAR-TL00", - "PAR-TL20", - "ANE-AL00", - "ANE-TL00", - "INE-AL00", - "INE-LX1", - "INE-LX1r", - "INE-LX2", - "INE-TL00" - ], - "nfcPos": { - "x0": 0.0, - "y0": 0.0, - "x1": 0.7679558011049724, - "y1": 0.0979020979020979 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI P10-Serie", - "modelNames": [ - "VTR-AL00", - "VTR-L09", - "VTR-L29", - "VTR-TL00", - "VKY-AL00", - "VKY-L09", - "VKY-L29", - "VKY-TL00", - "WAS-L03T", - "WAS-LX1", - "WAS-LX1A", - "WAS-LX2", - "WAS-LX2J", - "WAS-LX3" - ], - "nfcPos": { - "x0": 0.1707317073170732, - "y0": 0.0, - "x1": 0.5951219512195122, - "y1": 0.07209302325581396 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI P20-Serie", - "modelNames": [ - "ANE-LX2J", - "HWV32", - "EML-AL00", - "EML-L09", - "EML-L29", - "EML-TL00", - "HW-01K", - "CLT-AL00", - "CLT-AL00l", - "CLT-AL01", - "CLT-L04", - "CLT-L09", - "CLT-L29", - "CLT-TL00", - "CLT-TL01", - "ANE-LX1", - "ANE-LX2", - "ANE-LX3", - "CLT-L09", - "CLT-L29" - ], - "nfcPos": { - "x0": 0.0871794871794872, - "y0": 0.02546296296296296, - "x1": 0.7128205128205128, - "y1": 0.2708333333333333 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI P30-Serie", - "modelNames": [ - "ELE-AL00", - "ELE-L04", - "ELE-L09", - "ELE-L14", - "ELE-L29", - "ELE-L39", - "ELE-L49", - "ELE-TL00", - "HWV33", - "MAR-LX1A", - "MAR-LX1Am", - "MAR-LX1B", - "MAR-LX1M", - "MAR-LX1Mm", - "MAR-LX2", - "MAR-LX2B", - "MAR-LX2m", - "MAR-LX3A", - "MAR-LX3Am", - "MAR-LX3Bm", - "ELE-L09", - "HW-02L", - "VOG-AL00", - "VOG-AL10", - "VOG-L04", - "VOG-L09", - "VOG-L29", - "VOG-TL00", - "MAR-LX2J" - ], - "nfcPos": { - "x0": 0.07853403141361259, - "y0": 0.020737327188940093, - "x1": 0.6073298429319371, - "y1": 0.2557603686635945 - } - }, - { - "manufacturer": "Huawei", - "marketingName": "HUAWEI P40-Serie", - "modelNames": [ - "ELS-NX9", - "ELS-N04", - "ELS-AN00", - "ELS-TN00", - "JNY-L21A", - "JNY-L01A", - "JNY-L21B", - "JNY-L22A", - "JNY-L02A", - "JNY-L22B", - "JNY-LX1", - "ANA-AN00", - "ANA-TN00", - "ANA-NX9", - "ANA-LX4", - "ELS-N39", - "ELS-AN10", - "CDY-NX9A", - "ANA-AL00", - "ART-L28", - "ART-L29", - "ART-L29N" - ], - "nfcPos": { - "x0": 0.032573289902280145, - "y0": 0.04495912806539509, - "x1": 0.501628664495114, - "y1": 0.3201634877384196 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A32 5G", - "modelNames": [ - "SCG08", - "SM-A326B", - "SM-A326BR", - "SM-A326U", - "SM-A326U1", - "SM-A326W", - "SM-S326DL" - ], - "nfcPos": { - "x0": 0.0699300699300699, - "y0": 0.29354838709677417, - "x1": 0.8951048951048951, - "y1": 0.603225806451613 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A42 5G", - "modelNames": [ - "SM-A4260", - "SM-A426B", - "SM-A426N", - "SM-A426U", - "SM-A426U1", - "SM-S426DL" - ], - "nfcPos": { - "x0": 0.049645390070921946, - "y0": 0.26129032258064516, - "x1": 0.9290780141843972, - "y1": 0.6903225806451613 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A50s", - "modelNames": [ - "SM-A5070", - "SM-A507FN" - ], - "nfcPos": { - "x0": 0.217687074829932, - "y0": 0.1064516129032258, - "x1": 0.7006802721088435, - "y1": 0.3064516129032258 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A51", - "modelNames": [ - "SM-A515F", - "SM-A515U", - "SM-A515U1", - "SM-A515W", - "SM-S515DL" - ], - "nfcPos": { - "x0": 0.08450704225352113, - "y0": 0.08108108108108109, - "x1": 0.6056338028169015, - "y1": 0.2905405405405405 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A52 5G", - "modelNames": [ - "SC-53B", - "SM-A5260", - "SM-A526B", - "SM-A526N", - "SM-A526U", - "SM-A526U1", - "SM-A526W" - ], - "nfcPos": { - "x0": 0.03597122302158273, - "y0": 0.0, - "x1": 0.5611510791366907, - "y1": 0.28378378378378377 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A60", - "modelNames": [ - "SM-A6060", - "SM-A606Y" - ], - "nfcPos": { - "x0": 0.1842105263157895, - "y0": 0.13306451612903225, - "x1": 0.7456140350877193, - "y1": 0.3225806451612903 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A70", - "modelNames": [ - "SM-A7050", - "SM-A705F", - "SM-A705FN", - "SM-A705GM", - "SM-A705MN", - "SM-A705U", - "SM-A705W", - "SM-A705YN" - ], - "nfcPos": { - "x0": 0.2206896551724138, - "y0": 0.12903225806451613, - "x1": 0.7655172413793103, - "y1": 0.3064516129032258 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A71", - "modelNames": [ - "SM-A715F", - "SM-A715W" - ], - "nfcPos": { - "x0": 0.07638888888888884, - "y0": 0.050335570469798654, - "x1": 0.5972222222222222, - "y1": 0.2684563758389262 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A8+", - "modelNames": [ - "SM-A730F", - "SM-A730X" - ], - "nfcPos": { - "x0": 0.1095890410958904, - "y0": 0.3076923076923077, - "x1": 0.8561643835616438, - "y1": 0.5737179487179487 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A8", - "modelNames": [ - "SCV32", - "SM-A800F", - "SM-A800YZ", - "SM-A800S", - "SM-A800I", - "SM-A800IZ", - "SM-A8000", - "SM-A800X", - "SM-G885F", - "SM-G885Y", - "SM-G8850", - "SM-G885S", - "SM-A810F", - "SM-A810YZ", - "SM-A810S", - "SM-A530F", - "SM-A530X", - "SM-A530W", - "SM-A530N" - ], - "nfcPos": { - "x0": 0.19424460431654678, - "y0": 0.06752411575562701, - "x1": 0.7769784172661871, - "y1": 0.21221864951768488 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A80", - "modelNames": [ - "SM-A8050", - "SM-A805F", - "SM-A805N" - ], - "nfcPos": { - "x0": 0.13013698630136983, - "y0": 0.18387096774193548, - "x1": 0.8356164383561644, - "y1": 0.47419354838709676 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A8s", - "modelNames": [ - "SM-G887F", - "SM-G8870" - ], - "nfcPos": { - "x0": 0.25, - "y0": 0.10240963855421686, - "x1": 0.7714285714285715, - "y1": 0.3072289156626506 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A9 (2018)", - "modelNames": [ - "SM-A920F", - "SM-A920N" - ], - "nfcPos": { - "x0": 0.04402515723270439, - "y0": 0.03115264797507788, - "x1": 0.9559748427672956, - "y1": 0.4143302180685358 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy C5 Pro", - "modelNames": [ - "SM-C5010", - "SM-C5018" - ], - "nfcPos": { - "x0": 0.23076923076923073, - "y0": 0.08012820512820513, - "x1": 0.6474358974358974, - "y1": 0.2403846153846154 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy C7 Pro", - "modelNames": [ - "SM-C701F", - "SM-C7010", - "SM-C7018" - ], - "nfcPos": { - "x0": 0.27338129496402874, - "y0": 0.0641025641025641, - "x1": 0.7122302158273381, - "y1": 0.21794871794871795 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy C9 Pro", - "modelNames": [ - "SM-C900F", - "SM-C900Y", - "SM-C9000", - "SM-C9008", - "SM-C900X" - ], - "nfcPos": { - "x0": 0.22857142857142854, - "y0": 0.07333333333333333, - "x1": 0.6928571428571428, - "y1": 0.20333333333333334 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Fold", - "modelNames": [ - "SCV44", - "SM-F9000", - "SM-F900F", - "SM-F900U", - "SM-F900U1", - "SM-F900W" - ], - "nfcPos": { - "x0": 0.15315315315315314, - "y0": 0.38387096774193546, - "x1": 0.9099099099099099, - "y1": 0.6387096774193548 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note10 Lite", - "modelNames": [ - "SM-N770F" - ], - "nfcPos": { - "x0": 0.10416666666666663, - "y0": 0.08389261744966443, - "x1": 0.5555555555555556, - "y1": 0.2953020134228188 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note10+", - "modelNames": [ - "SC-01M", - "SCV45", - "SM-N9750", - "SM-N975C", - "SM-N975U", - "SM-N975U1", - "SM-N975W", - "SM-N975F" - ], - "nfcPos": { - "x0": 0.09547738693467334, - "y0": 0.06129032258064516, - "x1": 0.542713567839196, - "y1": 0.35161290322580646 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note10", - "modelNames": [ - "SM-N970F", - "SM-N9700", - "SM-N970U", - "SM-N970U1", - "SM-N970W" - ], - "nfcPos": { - "x0": 0.08374384236453203, - "y0": 0.06129032258064516, - "x1": 0.5270935960591133, - "y1": 0.3419354838709677 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note20 5G", - "modelNames": [ - "SM-N9810", - "SM-N981N", - "SM-N981U", - "SM-N981U1", - "SM-N981W", - "SM-N981B" - ], - "nfcPos": { - "x0": 0.10516252390057357, - "y0": 0.4105263157894737, - "x1": 0.9082217973231358, - "y1": 0.7140350877192982 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note20 Ultra 5G", - "modelNames": [ - "SC-53A", - "SCG06", - "SM-N9860", - "SM-N986N", - "SM-N986U", - "SM-N986U1", - "SM-N986W", - "SM-N986B" - ], - "nfcPos": { - "x0": 0.06691449814126393, - "y0": 0.34509466437177283, - "x1": 0.9219330855018587, - "y1": 0.6858864027538726 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note5", - "modelNames": [ - "SM-N9208", - "SM-N920C", - "SM-N920F", - "SM-N920G", - "SM-N920I", - "SM-N920X", - "SM-N920R7", - "SAMSUNG-SM-N920A", - "SM-N920W8", - "SM-N9200", - "SM-N9208", - "SM-N9200", - "SM-N920K", - "SM-N920L", - "SM-N920R6", - "SM-N920S", - "SM-N920P", - "SM-N920T", - "SM-N920R4", - "SM-N920V" - ], - "nfcPos": { - "x0": 0.09219858156028371, - "y0": 0.4180064308681672, - "x1": 0.9787234042553191, - "y1": 0.77491961414791 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note8", - "modelNames": [ - "SC-01K", - "SCV37", - "SM-N950F", - "SM-N950N", - "SM-N950XN", - "SM-N950U", - "SM-N9500", - "SM-N9508", - "SM-N950W", - "SM-N950U1" - ], - "nfcPos": { - "x0": 0.18055555555555558, - "y0": 0.24666666666666667, - "x1": 0.8402777777777778, - "y1": 0.6266666666666667 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Note9", - "modelNames": [ - "SC-01L", - "SCV40", - "SM-N960F", - "SM-N960N", - "SM-N9600", - "SM-N960W", - "SM-N960U", - "SM-N960U1" - ], - "nfcPos": { - "x0": 0.23239436619718312, - "y0": 0.3389261744966443, - "x1": 0.823943661971831, - "y1": 0.5906040268456376 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S10+", - "modelNames": [ - "SC-04L", - "SCV42", - "SM-G975F", - "SM-G975N", - "SM-G9750", - "SM-G9758", - "SM-G975U", - "SM-G975U1", - "SM-G975W" - ], - "nfcPos": { - "x0": 0.1806451612903226, - "y0": 0.3383233532934132, - "x1": 0.8129032258064516, - "y1": 0.6347305389221557 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S10", - "modelNames": [ - "SC-03L", - "SCV41", - "SM-G973F", - "SM-G973N", - "SM-G9730", - "SM-G9738", - "SM-G973C", - "SM-G973U", - "SM-G973U1", - "SM-G973W" - ], - "nfcPos": { - "x0": 0.05031446540880502, - "y0": 0.2433234421364985, - "x1": 0.8050314465408805, - "y1": 0.6468842729970327 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S10e", - "modelNames": [ - "SM-G970F", - "SM-G970N", - "SM-G9700", - "SM-G9708", - "SM-G970U", - "SM-G970U1", - "SM-G970W" - ], - "nfcPos": { - "x0": 0.20370370370370372, - "y0": 0.322884012539185, - "x1": 0.8024691358024691, - "y1": 0.5987460815047022 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S20 FE", - "modelNames": [ - "SM-G780G", - "SM-G780F" - ], - "nfcPos": { - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S20 Ultra", - "modelNames": [], - "nfcPos": { - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S20+", - "modelNames": [ - "SM-G985F" - ], - "nfcPos": { - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S20", - "modelNames": [ - "SM-G980F" - ], - "nfcPos": { - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S21 5G", - "modelNames": [ - "SC-51B", - "SCG09", - "SM-G9910", - "SM-G991Q", - "SM-G991U1", - "SM-G991W", - "SM-G991B", - "SM-G991N" - ], - "nfcPos": { - "x0": 0.04929577464788737, - "y0": 0.46308724832214765, - "x1": 0.9436619718309859, - "y1": 0.7651006711409396 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S21 FE 5G", - "modelNames": [ - "SM-G9900", - "SM-G990B", - "SM-G990B2", - "SM-G990U", - "SM-G990U1", - "SM-G990U2", - "SM-G990U3", - "SM-G990W", - "SM-G990W2", - "SM-G990E" - ], - "nfcPos": { - "x0": 0.08904109589041098, - "y0": 0.28619528619528617, - "x1": 0.9178082191780822, - "y1": 0.6531986531986532 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S21 Ultra 5G", - "modelNames": [ - "SC-52B", - "SM-G9980", - "SM-G998U", - "SM-G998U1", - "SM-G998W", - "SM-G998B", - "SM-G998N" - ], - "nfcPos": { - "x0": 0.02877697841726623, - "y0": 0.40604026845637586, - "x1": 0.9568345323741008, - "y1": 0.7550335570469798 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S21+ 5G", - "modelNames": [ - "SCG10", - "SM-G9960", - "SM-G996U1", - "SM-G996W", - "SM-G996B", - "SM-G996N" - ], - "nfcPos": { - "x0": 0.035211267605633756, - "y0": 0.39932885906040266, - "x1": 0.971830985915493, - "y1": 0.7550335570469798 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S22 Ultra", - "modelNames": [ - "SC-52C", - "SCG14", - "SM-S9080", - "SM-S908E", - "SM-S908N", - "SM-S908U", - "SM-S908U1", - "SM-S908W", - "SM-S908B" - ], - "nfcPos": { - "x0": 0.007633587786259555, - "y0": 0.34838709677419355, - "x1": 1.0, - "y1": 0.7741935483870968 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S22+", - "modelNames": [ - "SM-S9060", - "SM-S906E", - "SM-S906N", - "SM-S906U", - "SM-S906U1", - "SM-S906W", - "SM-S906B" - ], - "nfcPos": { - "x0": 0.05405405405405406, - "y0": 0.3258064516129032, - "x1": 0.9594594594594594, - "y1": 0.7548387096774194 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S22", - "modelNames": [ - "SC-51C", - "SCG13", - "SM-S9010", - "SM-S901E", - "SM-S901N", - "SM-S901U", - "SM-S901U1", - "SM-S901W", - "SM-S901B" - ], - "nfcPos": { - "x0": 0.05921052631578949, - "y0": 0.35161290322580646, - "x1": 0.9144736842105263, - "y1": 0.7580645161290323 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S6 edge+", - "modelNames": [ - "SM-G9287", - "SM-G928F", - "SM-G928G" - ], - "nfcPos": { - "x0": 0.27922077922077926, - "y0": 0.36012861736334406, - "x1": 0.7272727272727273, - "y1": 0.7234726688102894 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S7 edge", - "modelNames": [ - "SM-G935F", - "SM-G935L", - "SM-G9350", - "SM-G935U" - ], - "nfcPos": { - "x0": 0.09999999999999998, - "y0": 0.255663430420712, - "x1": 0.9, - "y1": 0.627831715210356 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S7", - "modelNames": [ - "SM-G930F", - "SM-G930X", - "SM-G930W8", - "SM-G930K", - "SM-G930L", - "SM-G930S", - "SM-G930R7", - "SAMSUNG-SM-G930AZ", - "SAMSUNG-SM-G930A", - "SM-G930VC", - "SM-G9300", - "SM-G9308", - "SM-G930R6", - "SM-G930T1", - "SM-G930P", - "SM-G930VL", - "SM-G930T", - "SM-G930U", - "SM-G930R4", - "SM-G930V" - ], - "nfcPos": { - "x0": 0.06578947368421051, - "y0": 0.26282051282051283, - "x1": 0.9539473684210527, - "y1": 0.6217948717948718 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S8+", - "modelNames": [ - "SC-03J", - "SCV35", - "SM-G955F", - "SM-G955N", - "SM-G955W", - "SM-G9550", - "SM-G955U", - "SM-G955U1" - ], - "nfcPos": { - "x0": 0.15602836879432624, - "y0": 0.3054662379421222, - "x1": 0.8723404255319149, - "y1": 0.6334405144694534 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S8", - "modelNames": [ - "SC-02J", - "SCV36", - "SM-G950F", - "SM-G950N", - "SM-G950W", - "SM-G9500", - "SM-G9508", - "SM-G950U", - "SM-G950U1" - ], - "nfcPos": { - "x0": 0.1448275862068965, - "y0": 0.36538461538461536, - "x1": 0.8551724137931034, - "y1": 0.6955128205128205 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S9+", - "modelNames": [ - "SC-03K", - "SCV39", - "SM-G965F", - "SM-G965N", - "SM-G9650", - "SM-G965W", - "SM-G965U", - "SM-G965U1" - ], - "nfcPos": { - "x0": 0.11564625850340138, - "y0": 0.38782051282051283, - "x1": 0.8707482993197279, - "y1": 0.7275641025641025 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S9", - "modelNames": [ - "SC-02K", - "SCV38", - "SM-G960F", - "SM-G960N", - "SM-G9600", - "SM-G9608", - "SM-G960W", - "SM-G960U", - "SM-G960U1" - ], - "nfcPos": { - "x0": 0.12328767123287676, - "y0": 0.3557692307692308, - "x1": 0.863013698630137, - "y1": 0.6826923076923077 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Flip 5G", - "modelNames": [ - "SCG04", - "SM-F7070", - "SM-F707B", - "SM-F707N", - "SM-F707U", - "SM-F707U1", - "SM-F707W" - ], - "nfcPos": { - "x0": 0.12745098039215685, - "y0": 0.6365979381443299, - "x1": 0.8333333333333334, - "y1": 0.884020618556701 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Flip LTE", - "modelNames": [ - "SCV47", - "SM-F7000", - "SM-F700F", - "SM-F700N", - "SM-F700U", - "SM-F700U1", - "SM-F700W" - ], - "nfcPos": { - "x0": 0.19565217391304346, - "y0": 0.6806451612903226, - "x1": 0.8043478260869565, - "y1": 0.9 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Flip3 5G", - "modelNames": [ - "SC-54B", - "SCG12", - "SM-F7110", - "SM-F711B", - "SM-F711N", - "SM-F711U", - "SM-F711U1", - "SM-F711W" - ], - "nfcPos": { - "x0": 0.08088235294117652, - "y0": 0.5973154362416108, - "x1": 0.9264705882352942, - "y1": 0.912751677852349 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Flip4 5G", - "modelNames": [ - "SC-54C", - "SCG17", - "SM-F7210", - "SM-F721B", - "SM-F721C", - "SM-F721N", - "SM-F721U", - "SM-F721U1", - "SM-F721W" - ], - "nfcPos": { - "x0": 0.06617647058823528, - "y0": 0.5709677419354838, - "x1": 0.9264705882352942, - "y1": 0.8774193548387097 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Fold2 5G", - "modelNames": [ - "SM-F9160", - "SM-F916B", - "SM-F916N", - "SM-F916Q", - "SM-F916U", - "SM-F916U1", - "SM-F916W" - ], - "nfcPos": { - "x0": 0.12959381044487428, - "y0": 0.32319078947368424, - "x1": 0.9226305609284333, - "y1": 0.6077302631578947 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Fold3 5G", - "modelNames": [ - "SC-55B", - "SCG11", - "SM-F9260", - "SM-F926B", - "SM-F926N", - "SM-F926U", - "SM-F926U1", - "SM-F926W" - ], - "nfcPos": { - "x0": 0.10526315789473684, - "y0": 0.4129032258064516, - "x1": 0.9473684210526316, - "y1": 0.7516129032258064 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy Z Fold4 5G", - "modelNames": [ - "SC-55C", - "SCG16", - "SM-F9360", - "SM-F936B", - "SM-F936N", - "SM-F936U", - "SM-F936U1", - "SM-F936W" - ], - "nfcPos": { - "x0": 0.10447761194029848, - "y0": 0.45806451612903226, - "x1": 0.9328358208955224, - "y1": 0.7967741935483871 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel (2016)", - "modelNames": [ - "Pixel" - ], - "nfcPos": { - "x0": 0.5182481751824817, - "y0": 0.0, - "x1": 0.6496350364963503, - "y1": 0.011673151750972763 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 2 (2017)", - "modelNames": [ - "Pixel 2" - ], - "nfcPos": { - "x0": 0.33884297520661155, - "y0": 0.0622568093385214, - "x1": 0.487603305785124, - "y1": 0.13229571984435798 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 3 (2018)", - "modelNames": [ - "Pixel 3" - ], - "nfcPos": { - "x0": 0.17098445595854928, - "y0": 0.12224938875305623, - "x1": 0.7305699481865284, - "y1": 0.3863080684596577 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 3a (2019)", - "modelNames": [ - "Pixel 3a" - ], - "nfcPos": { - "x0": 0.2195121951219512, - "y0": 0.14788732394366197, - "x1": 0.7463414634146341, - "y1": 0.4014084507042254 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 4 (2019)", - "modelNames": [ - "Pixel 4" - ], - "nfcPos": { - "x0": 0.10188679245283017, - "y0": 0.15845070422535212, - "x1": 0.41132075471698115, - "y1": 0.3028169014084507 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 4a (2020)", - "modelNames": [ - "Pixel 4a" - ], - "nfcPos": { - "x0": 0.4957983193277311, - "y0": 0.39096267190569745, - "x1": 0.6428571428571428, - "y1": 0.45972495088408644 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 4a (5G)", - "modelNames": [ - "Pixel 4a (5G)" - ], - "nfcPos": { - "x0": 0.44339622641509435, - "y0": 0.3858093126385809, - "x1": 0.5849056603773585, - "y1": 0.4523281596452328 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 5", - "modelNames": [ - "Pixel 5" - ], - "nfcPos": { - "x0": 0.4416243654822335, - "y0": 0.34988179669030733, - "x1": 0.5939086294416244, - "y1": 0.42080378250591016 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 5a (5G)", - "modelNames": [ - "Pixel 5a" - ], - "nfcPos": { - "x0": 0.44339622641509435, - "y0": 0.3858093126385809, - "x1": 0.5849056603773585, - "y1": 0.4523281596452328 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 6 Pro", - "modelNames": [ - "Pixel 6 Pro" - ], - "nfcPos": { - "x0": 0.43434343434343436, - "y0": 0.5373831775700935, - "x1": 0.5808080808080809, - "y1": 0.6051401869158879 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 6", - "modelNames": [ - "Pixel 6" - ], - "nfcPos": { - "x0": 0.45744680851063835, - "y0": 0.4737903225806452, - "x1": 0.6117021276595744, - "y1": 0.532258064516129 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 6a", - "modelNames": [ - "Pixel 6a" - ], - "nfcPos": { - "x0": 0.45789473684210524, - "y0": 0.4778225806451613, - "x1": 0.6105263157894737, - "y1": 0.5362903225806451 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 7 Pro", - "modelNames": [ - "Pixel 7 Pro" - ], - "nfcPos": { - "x0": 0.4097222222222222, - "y0": 0.2602291325695581, - "x1": 0.5902777777777778, - "y1": 0.3453355155482815 - } - }, - { - "manufacturer": "Google", - "marketingName": "Pixel 7", - "modelNames": [ - "Pixel 7" - ], - "nfcPos": { - "x0": 0.40909090909090906, - "y0": 0.26143790849673204, - "x1": 0.6, - "y1": 0.35294117647058826 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy A34 5G", - "modelNames": [ - "SM-A3460", - "SM-A346B", - "SM-A346E", - "SM-A346M", - "SM-A346N" - ], - "nfcPos": { - "x0": 0.04054054054054057, - "y0": 0.016835016835016835, - "x1": 0.7297297297297297, - "y1": 0.27946127946127947 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S23 Ultra", - "modelNames": [ - "SC-52D", - "SM-S9180", - "SM-S918B", - "SM-S918N", - "SM-S918U", - "SM-S918U1", - "SM-S918W" - ], - "nfcPos": { - "x0": 0.05405405405405406, - "y0": 0.3468013468013468, - "x1": 0.9324324324324325, - "y1": 0.8316498316498316 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S23+", - "modelNames": [ - "SM-S9160", - "SM-S916B", - "SM-S916N", - "SM-S916U", - "SM-S916U1", - "SM-S916W" - ], - "nfcPos": { - "x0": 0.03355704697986572, - "y0": 0.33557046979865773, - "x1": 0.9463087248322147, - "y1": 0.7550335570469798 - } - }, - { - "manufacturer": "Samsung", - "marketingName": "Samsung Galaxy S23", - "modelNames": [ - "SC-51D", - "SM-S9110", - "SM-S911B", - "SM-S911C", - "SM-S911N", - "SM-S911U", - "SM-S911U1", - "SM-S911W" - ], - "nfcPos": { - "x0": 0.04054054054054057, - "y0": 0.34459459459459457, - "x1": 0.9459459459459459, - "y1": 0.7533783783783784 - } - } -] \ No newline at end of file diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt index 5d2af20d..0f892ef2 100644 --- a/app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt @@ -86,7 +86,9 @@ class SettingsUseCaseTest { welcomeDrawerShown = false, mainScreenTooltipsShown = true, mlKitAccepted = false, - screenShotsAllowed = false + screenShotsAllowed = false, + trackingAllowed = false, + userHasAcceptedIntegrityNotOk = false ) ) initSettings() @@ -106,7 +108,10 @@ class SettingsUseCaseTest { welcomeDrawerShown = true, mainScreenTooltipsShown = true, mlKitAccepted = false, - screenShotsAllowed = false + screenShotsAllowed = false, + trackingAllowed = false, + userHasAcceptedIntegrityNotOk = false + ) ) initSettings() diff --git a/app/demo-mode/build.gradle.kts b/app/demo-mode/build.gradle.kts index 6ecd307c..bc94fc4c 100644 --- a/app/demo-mode/build.gradle.kts +++ b/app/demo-mode/build.gradle.kts @@ -18,10 +18,6 @@ plugins { id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") } -tasks.named("preBuild") { - dependsOn(":ktlint", ":detekt") -} - licenseReport { generateCsvReport = false generateHtmlReport = false @@ -32,7 +28,7 @@ licenseReport { android { namespace = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.demomode" defaultConfig { - testApplicationId = "de.gematik.ti.erp.app..demomode.test" + testApplicationId = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.demomode.test" } kotlinOptions { jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET @@ -48,6 +44,9 @@ android { it.name.startsWith("kapt") }.map { it.name } } + buildTypes { + create("minifiedDebug") + } // disable build config for demo-mode since we use only from features. // If needed we need a new-namespace buildFeatures { @@ -120,5 +119,9 @@ dependencies { secrets { defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() - ) "ci-overrides.properties" else "gradle.properties" + ) { + "ci-overrides.properties" + } else { + "gradle.properties" + } } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt similarity index 68% rename from app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt rename to app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt index 42444ddb..c7c6db09 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeObserver.kt @@ -18,20 +18,29 @@ package de.gematik.ti.erp.app.demomode -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.flow.MutableStateFlow -abstract class DemoModeActivity : AppCompatActivity() { +interface DemoModeObserver { - private val isDemoMode by lazy { MutableStateFlow(false) } + val isDemoMode: MutableStateFlow - fun setAsDemoMode() { + fun setAsDemoMode() + + fun cancelDemoMode() + + fun isDemoMode(): Boolean +} + +class DefaultDemoModeObserver : DemoModeObserver { + + override val isDemoMode by lazy { MutableStateFlow(false) } + override fun setAsDemoMode() { isDemoMode.value = true } - fun cancelDemoMode() { + override fun cancelDemoMode() { isDemoMode.value = false } - fun isDemoMode() = isDemoMode.value + override fun isDemoMode() = isDemoMode.value } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt index 64eccf09..869a610f 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt @@ -25,12 +25,15 @@ import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoS import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoSyncedPrescription.syncedTask import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile01 import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile02 +import de.gematik.ti.erp.app.demomode.model.DemoModeProfile import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication import de.gematik.ti.erp.app.idp.api.models.PairingData import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry import de.gematik.ti.erp.app.orders.repository.CachedPharmacy import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.protocol.model.AuditEventData import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.days @@ -42,99 +45,109 @@ class DemoModeDataSource { /** * Data sources for the [profiles] created in the demo-mode */ - val profiles = MutableStateFlow(mutableListOf(demoProfile01, demoProfile02)) + val profiles: MutableStateFlow> = + MutableStateFlow(mutableListOf(demoProfile01, demoProfile02)) /** * Data sources for the [syncedTasks] created in the demo-mode */ - val syncedTasks = MutableStateFlow( - mutableListOf( - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 1), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 2), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.InProgress, index = 3), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Canceled, index = 4), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 5), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 6), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 7), - syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 8), + val syncedTasks: MutableStateFlow> = + MutableStateFlow( + mutableListOf( + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 1), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 2), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.InProgress, index = 3), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Canceled, index = 4), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 5), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 6), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 7), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 8), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 9), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 10), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 11), - syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.InProgress, index = 12), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 9), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 10), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 11), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.InProgress, index = 12) + ) ) - ) /** * Data sources for the [scannedTasks] created in the demo-mode */ - val scannedTasks = MutableStateFlow(mutableListOf(demoScannedTask01, demoScannedTask02)) + val scannedTasks: MutableStateFlow> = + MutableStateFlow(mutableListOf(demoScannedTask01, demoScannedTask02)) /** * Data sources for the [favoritePharmacies] created in the demo-mode */ - val favoritePharmacies = MutableStateFlow(mutableListOf(demoFavouritePharmacy)) + val favoritePharmacies: MutableStateFlow> = + MutableStateFlow(mutableListOf(demoFavouritePharmacy)) /** * Data sources for the [oftenUsedPharmacies] created in the demo-mode */ - val oftenUsedPharmacies = MutableStateFlow(mutableListOf()) + val oftenUsedPharmacies: MutableStateFlow> = + MutableStateFlow(mutableListOf()) /** * Data sources for the [auditEvents] created in the demo-mode */ - val auditEvents = MutableStateFlow( - mutableListOf( - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadPrescription(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadPrescription(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadPrescription(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadPrescription(), - DemoAuditEventInfo.downloadDispense(), - DemoAuditEventInfo.downloadPrescription() + val auditEvents: MutableStateFlow> = + MutableStateFlow( + mutableListOf( + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription() + ) ) - ) /** * Data sources for the [communications] created in the demo-mode, * this is used as the source for communication between the user, pharmacy and the doctor */ - val communications = MutableStateFlow(mutableListOf()) + val communications: MutableStateFlow> = + MutableStateFlow(mutableListOf()) /** * Data source for the a [profileCommunicationLog] communication log that a particular profile has downloaded the information */ - val profileCommunicationLog = MutableStateFlow(mutableMapOf("no-profile-id" to false)) + val profileCommunicationLog: MutableStateFlow> = + MutableStateFlow(mutableMapOf("no-profile-id" to false)) /** * Data source for the [cachedPharmacies] used for communications */ - val cachedPharmacies = MutableStateFlow(mutableListOf()) + val cachedPharmacies: MutableStateFlow> = + MutableStateFlow(mutableListOf()) /** * Data source for the connected device [pairedDevices] that will be shown to the user */ - val pairedDevices = MutableStateFlow( - mutableListOf( - PairingResponseEntry( - name = "Pixel 10", - creationTime = Clock.System.now().minus(10.days).toEpochMilliseconds(), - signedPairingData = "pairing.data" - ) to - PairingData( - subjectPublicKeyInfoOfSecureElement = "subjectPublicKeyInfoOfSecureElement", - keyAliasOfSecureElement = "keyAliasOfSecureElement", - productName = "productName", - serialNumberOfHealthCard = "serialNumberOfHealthCard", - issuerOfHealthCard = "issuerOfHealthCard", - subjectPublicKeyInfoOfHealthCard = "subjectPublicKeyInfoOfHealthCard", - validityUntilOfHealthCard = Clock.System.now().plus(365.days).toEpochMilliseconds() - ) + val pairedDevices: MutableStateFlow>> = + MutableStateFlow( + mutableListOf( + PairingResponseEntry( + name = "Pixel 10", + creationTime = Clock.System.now().minus(10.days).toEpochMilliseconds(), + signedPairingData = "pairing.data" + ) to + PairingData( + subjectPublicKeyInfoOfSecureElement = "subjectPublicKeyInfoOfSecureElement", + keyAliasOfSecureElement = "keyAliasOfSecureElement", + productName = "productName", + serialNumberOfHealthCard = "serialNumberOfHealthCard", + issuerOfHealthCard = "issuerOfHealthCard", + subjectPublicKeyInfoOfHealthCard = "subjectPublicKeyInfoOfHealthCard", + validityUntilOfHealthCard = Clock.System.now().plus(365.days).toEpochMilliseconds() + ) + ) ) - ) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt index 2e781d86..793da09e 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt @@ -42,7 +42,7 @@ object DemoAuditEventInfo { internal fun downloadDispense(taskId: String = UUID.randomUUID().toString()) = AuditEventData.AuditEvent( auditId = UUID.randomUUID().toString(), taskId = taskId, - description = "Max Mustermann dowloaded a medication dispense list", + description = "Sie haben eine Medikamentenabgabeliste heruntergeladen", timestamp = Clock.System.now().minus(randomInt.randomDuration()) ) @@ -50,7 +50,7 @@ object DemoAuditEventInfo { AuditEventData.AuditEvent( auditId = UUID.randomUUID().toString(), taskId = taskId, - description = "Max Mustermann dowloaded a prescription $taskId", + description = "Sie haben ein Rezept heruntergeladen mit taskId $taskId", timestamp = Clock.System.now().minus(randomInt.randomDuration()) ) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt index 73086360..84520423 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt @@ -24,7 +24,6 @@ import kotlin.time.Duration.Companion.minutes object DemoConstants { internal val randomTimeToday = Clock.System.now().minus((1..20).random().minutes) - internal val pastDate = Clock.System.now().minus((15..100).random().days) internal val longerRandomTimeToday = Clock.System.now().minus((2..58).random().minutes) internal const val PHARMACY_TELEMATIK_ID = "3-03.2.1006210000.10.795" internal const val SYNCED_TASK_PRESET = "110.000.002.345.863" diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt index 25fb7a94..48608340 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt @@ -299,30 +299,6 @@ object DemoPrescriptionInfo { } internal object DemoSyncedPrescription { - internal val demoSyncedPrescription01 = SyncedTaskData.SyncedTask(/**/ - profileId = demoProfile01.id, - taskId = "${SYNCED_TASK_PRESET}.1", - isIncomplete = false, - pvsIdentifier = DEMO_MODE_IDENTIFIER, - accessCode = DEMO_MODE_IDENTIFIER, - lastModified = longerRandomTimeToday, - organization = ORGANIZATION, - practitioner = PRACTITIONER, - patient = PATIENT, - insuranceInformation = SyncedTaskData.InsuranceInformation( - name = null, - status = null - ), - expiresOn = EXPIRY_DATE, - acceptUntil = SHORT_EXPIRY_DATE, - authoredOn = NOW, - status = SyncedTaskData.TaskStatus.Ready, - medicationRequest = MEDICATION_REQUEST, - medicationDispenses = listOf(MEDICATION_DISPENSE), - communications = emptyList(), - failureToReport = "" - ) - internal fun syncedTask( profileIdentifier: ProfileIdentifier, status: SyncedTaskData.TaskStatus = SyncedTaskData.TaskStatus.Ready, diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt index 0f07270e..b802ffbd 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt @@ -16,12 +16,15 @@ * */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.demomode.datasource.data import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.EXPIRY_DATE import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.START_DATE +import de.gematik.ti.erp.app.demomode.model.DemoModeProfile import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.model.ProfilesData import kotlinx.datetime.Instant @@ -69,7 +72,14 @@ object DemoProfileInfo { isActive = true, color = ProfilesData.ProfileColorNames.SUN_DEW, insuranceType = InsuranceTypeV1.PKV, // Note: Private insurance account - avatar = ProfilesData.Avatar.FemaleDoctorWithPhone, + avatar = listOf( + ProfilesData.Avatar.FemaleDoctor, + ProfilesData.Avatar.FemaleDoctorWithPhone, + ProfilesData.Avatar.WomanWithHeadScarf, + ProfilesData.Avatar.WomanWithPhone, + ProfilesData.Avatar.Grandmother, + ProfilesData.Avatar.FemaleDeveloper + ).random(), lastAuthenticated = null ) @@ -80,31 +90,41 @@ object DemoProfileInfo { profileName = "Max Mustermann", isActive = false, insuranceType = InsuranceTypeV1.GKV, - avatar = ProfilesData.Avatar.OldManOfColor, + avatar = listOf( + ProfilesData.Avatar.OldManOfColor, + ProfilesData.Avatar.Grandfather, + ProfilesData.Avatar.ManWithPhone, + ProfilesData.Avatar.WheelchairUser, + ProfilesData.Avatar.MaleDoctorWithPhone + ).random(), lastAuthenticated = null ) private fun profile( profileName: String, isActive: Boolean = true, - color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.BLUE_MOON, - avatar: ProfilesData.Avatar = ProfilesData.Avatar.FemaleDeveloper, + color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.values().random(), + avatar: ProfilesData.Avatar = ProfilesData.Avatar.values().random(), insuranceType: InsuranceTypeV1 = InsuranceTypeV1.GKV, lastAuthenticated: Instant? = null, singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? = cardToken - ) = ProfilesData.Profile( - id = UUID.randomUUID().toString(), - name = profileName, - color = color, - avatar = avatar, - insuranceIdentifier = insuranceNumberGenerator(), - insuranceType = insuranceType, - insurantName = profileName, - insuranceName = HEALTH_INSURANCE_COMPANIES.random(), - singleSignOnTokenScope = singleSignOnTokenScope, - active = isActive, - lastAuthenticated = lastAuthenticated - ) + ): DemoModeProfile { + val uuid = UUID.randomUUID() + return DemoModeProfile( + demoModeId = uuid, + id = uuid.toString(), + name = profileName, + color = color, + avatar = avatar, + insuranceIdentifier = insuranceNumberGenerator(), + insuranceType = insuranceType, + insurantName = profileName, + insuranceName = HEALTH_INSURANCE_COMPANIES.random(), + singleSignOnTokenScope = singleSignOnTokenScope, + active = isActive, + lastAuthenticated = lastAuthenticated + ) + } internal fun String.create() = profile(profileName = this) } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt index 1ee15dbb..3fe7b09b 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt @@ -45,7 +45,6 @@ val demoModeModule = DI.Module("demoModeModule") { bindProvider { DemoDownloadCommunicationResource(instance()) } // only data source for demo mode bindSingleton { DemoModeDataSource() } - } fun DI.MainBuilder.demoModeOverrides() { diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt new file mode 100644 index 00000000..1d9e9164 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfile.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.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.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Instant +import java.util.UUID + +data class DemoModeProfile( + val demoModeId: UUID, + val id: ProfileIdentifier, + val color: ProfilesData.ProfileColorNames, + val avatar: ProfilesData.Avatar, + val personalizedImage: ByteArray? = null, + val name: String, + 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, + val active: Boolean = false, + val singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? +) + +fun MutableList.toProfiles() = map(DemoModeProfile::toProfile).toMutableList() + +fun DemoModeProfile.toProfile() = ProfilesData.Profile( + id = id, + color = color, + avatar = avatar, + personalizedImage = personalizedImage, + name = name, + insurantName = insurantName, + insuranceIdentifier = insuranceIdentifier, + insuranceName = insuranceName, + insuranceType = insuranceType, + lastAuthenticated = lastAuthenticated, + lastAuditEventSynced = lastAuditEventSynced, + lastTaskSynced = lastTaskSynced, + active = active, + singleSignOnTokenScope = singleSignOnTokenScope +) diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt index ec6fc505..c24b3ec5 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt @@ -87,4 +87,3 @@ data class DemoModeCommunicationPayloadContent( ) } } - diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt index 1ce5409d..938852f7 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt @@ -16,6 +16,8 @@ * */ +@file:Suppress("TooManyFunctions", "MagicNumber") + package de.gematik.ti.erp.app.demomode.repository.orders import de.gematik.ti.erp.app.api.ResourcePaging @@ -108,7 +110,6 @@ class DemoCommunicationRepository( syncedTasks.find { it.taskId == taskId } }.flowOn(dispatcher) - override fun loadScannedByTaskId(taskId: String): Flow = dataSource.scannedTasks.map { scannedTask -> scannedTask.find { it.taskId == taskId } @@ -234,14 +235,15 @@ class DemoCommunicationRepository( @OptIn(ExperimentalCoroutinesApi::class) private fun loadOrdersForActiveProfile() = findActiveProfile().flatMapLatest { loadOrdersByProfileId(it.id) } - /** * Method added so that demoModeProfile02 always loads with some communication * and for other profiles we have to add it. [downloadCommunications] method takes * in profileId so this can be changed to a different profile later too */ - private fun loadOrdersByProfileId(profileId: ProfileIdentifier): - Flow> = + @OptIn(ExperimentalCoroutinesApi::class) + private fun loadOrdersByProfileId( + profileId: ProfileIdentifier + ): Flow> = // For the profile 2 we load it with some existing communications if (profileId == DemoProfileInfo.demoProfile02.id) { dataSource.profileCommunicationLog diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt index 9b434e7c..98d689bf 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt @@ -101,7 +101,8 @@ class DemoPharmacyLocalDataSource( withContext(dispatcher) { dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { val favoritePharmacies = it.toMutableList() - favoritePharmacies.indexOfFirst { existingPharmacy -> existingPharmacy.telematikId == pharmacy.telematikId } + favoritePharmacies + .indexOfFirst { existingPharmacy -> existingPharmacy.telematikId == pharmacy.telematikId } .takeIf { index -> index != INDEX_OUT_OF_BOUNDS }?.let { index -> favoritePharmacies[index] = favoritePharmacies[index].copy(lastUsed = Clock.System.now()) favoritePharmacies @@ -127,7 +128,6 @@ class DemoPharmacyLocalDataSource( favoritePharmacy != null }.flowOn(dispatcher) - override suspend fun markAsRedeemed(taskId: String) { withContext(dispatcher) { dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt index 46b5b455..eda63295 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.withContext @@ -57,13 +58,13 @@ class DemoPrescriptionsRepository( } override fun scannedTasks(profileId: ProfileIdentifier): Flow> = dataSource.scannedTasks + .map { list -> list.filter { it.profileId == profileId } } override fun syncedTasks(profileId: ProfileIdentifier): Flow> = dataSource.syncedTasks.mapNotNull { taskList -> taskList.filter { it.profileId == profileId }.sortedBy { it.lastModified } }.flowOn(dispatcher) - override suspend fun redeemPrescription( profileId: ProfileIdentifier, communication: JsonElement, @@ -88,7 +89,8 @@ class DemoPrescriptionsRepository( } dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { val scannedList = it.toMutableList() - scannedList.removeIf { scannedItem -> scannedItem.taskId == taskId && scannedItem.profileId == profileId } + scannedList + .removeIf { scannedItem -> scannedItem.taskId == taskId && scannedItem.profileId == profileId } scannedList } Result.success(Unit) diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt index 0f0d7fc1..1188026d 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt @@ -16,6 +16,8 @@ * */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.demomode.repository.prescriptions import de.gematik.ti.erp.app.api.ResourcePaging.ResourceResult @@ -43,6 +45,4 @@ class DemoTaskRepository( ): Result> = Result.success(ResourceResult(0, 0)) override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = null - } - diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt index 88979bb4..4f267f03 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt @@ -20,6 +20,9 @@ package de.gematik.ti.erp.app.demomode.repository.profiles import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.create +import de.gematik.ti.erp.app.demomode.model.DemoModeProfile +import de.gematik.ti.erp.app.demomode.model.toProfile +import de.gematik.ti.erp.app.demomode.model.toProfiles import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.Add import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.NoAction import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.Remove @@ -28,20 +31,25 @@ import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.withContext import kotlinx.datetime.Instant +import java.util.UUID class DemoProfilesRepository( private val dataSource: DemoModeDataSource, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ProfileRepository { - override fun profiles() = dataSource.profiles + private fun demoModeProfiles(): MutableStateFlow> = dataSource.profiles + override fun profiles(): Flow> = demoModeProfiles() + .mapNotNull(MutableList::toProfiles) - override fun activeProfile() = profiles().mapNotNull { - it.find { profile -> profile.active } + override fun activeProfile() = demoModeProfiles().mapNotNull { + it.find { profile -> profile.active }?.toProfile() } override suspend fun saveProfile(profileName: String, activate: Boolean) { @@ -142,11 +150,13 @@ class DemoProfilesRepository( // Not for demo mode, will come later } - private fun MutableList.index(profileId: ProfileIdentifier) = + override suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean = false + + private fun MutableList.index(profileId: ProfileIdentifier) = indexOfFirst { profile -> profile.id == profileId } .takeIf { it != INDEX_OUT_OF_BOUNDS } - private fun MutableList.replace( + private fun MutableList.replace( profileId: ProfileIdentifier, activate: Boolean? = null, name: String? = null, @@ -155,10 +165,11 @@ class DemoProfilesRepository( avatar: ProfilesData.Avatar? = null, profileImage: ByteArray? = null, imageAction: ImageActions = NoAction - ): MutableList = + ): MutableList = index(profileId)?.let { index -> val existingProfile = this[index] this[index] = this[index].copy( + demoModeId = existingProfile.demoModeId, active = activate ?: existingProfile.active, name = name ?: existingProfile.name, color = color ?: existingProfile.color, @@ -173,13 +184,13 @@ class DemoProfilesRepository( this } ?: this - private fun List.deactivateAllProfiles() = + private fun List.deactivateAllProfiles() = mapNotNull { it.copy(active = false) }.toMutableList() - private fun MutableList.updateUUIDForChangeVisibility() = this - // map { it.copy(id = UUID.randomUUID().toString()) }.toMutableList() + private fun MutableList.updateUUIDForChangeVisibility() = + map { it.copy(demoModeId = UUID.randomUUID()) }.toMutableList() enum class ImageActions { Add, Remove, NoAction diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt index f82476bf..4c1767a0 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + package de.gematik.ti.erp.app.demomode.repository.protocol import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource @@ -19,5 +37,4 @@ class DemoAuditEventsRepository(private val dataSource: DemoModeDataSource) : Au ) return Result.success(mappingResult) } - -} \ No newline at end of file +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt index d30e447d..ec5dbf85 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt @@ -40,7 +40,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import com.google.accompanist.systemuicontroller.rememberSystemUiController -import de.gematik.ti.erp.app.demomode.DemoModeActivity +import de.gematik.ti.erp.app.demomode.DemoModeObserver @Suppress("ComposableNaming") @Composable @@ -49,8 +49,8 @@ fun checkForDemoMode( demoModeContent: @Composable ColumnScope.() -> Unit, appContent: @Composable ColumnScope.() -> Unit ) { - val activity = LocalContext.current.getAsDemoModeActivity() - val isDemoMode = (activity)?.isDemoMode() ?: false + val demoModeObserver = LocalContext.current.getDemoModeObserver() + val isDemoMode = demoModeObserver?.isDemoMode() ?: false val systemUiController = rememberSystemUiController() Column( @@ -77,7 +77,7 @@ fun DemoModeStatusBar( ) { Row( modifier = Modifier - .fillMaxWidth() + .then(modifier) .background(backgroundColor), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically @@ -108,4 +108,4 @@ private fun Context.getActivity(): AppCompatActivity? { return null } -internal fun Context.getAsDemoModeActivity(): DemoModeActivity? = getActivity() as? DemoModeActivity +internal fun Context.getDemoModeObserver(): DemoModeObserver? = getActivity() as? DemoModeObserver diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt index ed355270..470ebc83 100644 --- a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt @@ -92,11 +92,11 @@ class DemoIdpUseCase( // no implementation for demo mode } - override suspend fun getPairedDevices(profileId: ProfileIdentifier): Result>> = - withContext(dispatcher) { - val device = dataSource.pairedDevices.map { it.toList() }.first() - Result.success(device) - } + override suspend fun getPairedDevices(profileId: ProfileIdentifier): + Result>> = withContext(dispatcher) { + val device = dataSource.pairedDevices.map { it.toList() }.first() + Result.success(device) + } override suspend fun deletePairedDevice(profileId: ProfileIdentifier, deviceAlias: String): Result = withContext(dispatcher) { diff --git a/app/features/build.gradle.kts b/app/features/build.gradle.kts index b9dfd63e..a1bf8f97 100644 --- a/app/features/build.gradle.kts +++ b/app/features/build.gradle.kts @@ -18,10 +18,6 @@ plugins { id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") } -tasks.named("preBuild") { - dependsOn(":ktlint", ":detekt") -} - licenseReport { generateCsvReport = false generateHtmlReport = false @@ -48,11 +44,20 @@ android { it.name.startsWith("kapt") }.map { it.name } } + buildTypes { + val debug by getting { + isJniDebuggable = true + } + create("minifiedDebug") { + initWith(debug) + } + } } dependencies { implementation(project(":common")) implementation(project(":app:demo-mode")) + implementation("com.google.android.material:material:1.10.0") testImplementation(project(":common")) implementation(kotlin("stdlib")) implementation(kotlin("reflect")) @@ -154,6 +159,8 @@ dependencies { } tracking { implementation(contentSquare) + implementation(contentSquareCompose) + implementation(contentSquareErrorAnalysis) } maps { implementation(location) diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/DebugSettingsData.kt similarity index 97% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/DebugSettingsData.kt index 8dff7814..81e1b563 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/DebugSettingsData.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.data +package de.gematik.ti.erp.app.data import android.os.Parcelable import androidx.compose.runtime.Immutable diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/Environment.kt similarity index 94% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/Environment.kt index f02c0fbb..3be5c794 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/data/Environment.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.data +package de.gematik.ti.erp.app.data enum class Environment { PU, TU, RU, RUDEV, TR diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt index 944a9d39..032c213a 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt @@ -21,7 +21,7 @@ package de.gematik.ti.erp.app.di import android.content.SharedPreferences import androidx.core.content.edit import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.debug.data.Environment +import de.gematik.ti.erp.app.data.Environment class EndpointHelper( private val networkPrefs: SharedPreferences diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/navigation/DebugScreenNavigation.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/navigation/DebugScreenNavigation.kt new file mode 100644 index 00000000..4a63a258 --- /dev/null +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/navigation/DebugScreenNavigation.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen + +object DebugScreenNavigation { + object DebugMain : Routes("DebugMain") + object DebugRedeemWithoutFD : Routes("DebugRedeemWithoutFD") + object DebugPKV : Routes("DebugPKV") + object DebugBiometric : Routes(DebugTimeoutScreen::class::java.name) +} diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugLoadingButton.kt similarity index 98% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugLoadingButton.kt index 3bc733f5..366695d5 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugLoadingButton.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size @@ -24,11 +24,11 @@ import androidx.compose.material.Button import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugPKV.kt similarity index 99% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugPKV.kt index 557327c6..4e7fbdc7 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugPKV.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt similarity index 88% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt index b6bdb198..1e825adb 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreen.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import android.content.Intent import android.net.Uri @@ -69,6 +69,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -77,8 +78,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.debug.data.Environment +import de.gematik.ti.erp.app.data.Environment +import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.DebugScreenNavigation +import de.gematik.ti.erp.app.settings.ui.LabelButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AlertDialog @@ -111,33 +115,35 @@ fun DebugCard( modifier = modifier, shape = RoundedCornerShape(24.dp), backgroundColor = AppTheme.colors.neutral100, - elevation = 0.dp, - border = null + elevation = 10.dp ) { - Box { - Column( - Modifier.padding(PaddingDefaults.Medium), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - Text( - title, - style = MaterialTheme.typography.h6, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - SpacerMedium() - content() - } - onReset?.run { - IconButton( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(PaddingDefaults.Small), - onClick = onReset + Box( + contentAlignment = Alignment.Center, + content = { + Column( + Modifier.padding(PaddingDefaults.Medium), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) ) { - Icon(Icons.Rounded.Refresh, null) + Text( + title, + style = MaterialTheme.typography.h6, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + SpacerMedium() + content() + } + onReset?.run { + IconButton( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(PaddingDefaults.Small), + onClick = onReset + ) { + Icon(Icons.Rounded.Refresh, null) + } } } - } + ) } @Composable @@ -232,6 +238,9 @@ fun DebugScreen( }, onClickPKV = { navController.navigate(DebugScreenNavigation.DebugPKV.path()) + }, + onClickBioMetricSettings = { + navController.navigate(DebugScreenNavigation.DebugBiometric.path()) } ) } @@ -254,6 +263,12 @@ fun DebugScreen( ) } } + + composable(DebugScreenNavigation.DebugBiometric.route) { + DebugTimeoutScreen.Content { + navController.popBackStack() + } + } } } } @@ -394,7 +409,8 @@ private fun RedeemButton( fun DebugScreenMain( onBack: () -> Unit, onClickDirectRedemption: () -> Unit, - onClickPKV: () -> Unit + onClickPKV: () -> Unit, + onClickBioMetricSettings: () -> Unit ) { val viewModel by rememberViewModel() val listState = rememberLazyListState() @@ -423,7 +439,6 @@ fun DebugScreenMain( LaunchedEffect(Unit) { viewModel.state() } - val scope = rememberCoroutineScope() LazyColumn( state = listState, @@ -439,23 +454,29 @@ fun DebugScreenMain( DebugCard( title = "General" ) { - Button( - onClick = onClickDirectRedemption, - modifier = Modifier.fillMaxWidth() + LabelButton( + icon = painterResource(R.drawable.ic_finger), + text = "Biometric settings" ) { - Text(text = "Direct Redemption") + onClickBioMetricSettings() } - Button( - onClick = onClickPKV, - modifier = Modifier.fillMaxWidth() + LabelButton( + icon = painterResource(R.drawable.ic_qr_code), + text = "Direct Redemption" ) { - Text(text = "PKV") + onClickDirectRedemption() } - Button( - onClick = { viewModel.refreshPrescriptions() }, - modifier = Modifier.fillMaxWidth() + LabelButton( + icon = painterResource(R.drawable.ic_pkv), + text = "PKV" + ) { + onClickPKV() + } + LabelButton( + icon = painterResource(R.drawable.ic_prescription_refresh), + text = "Trigger Prescription Refresh" ) { - Text(text = "Trigger Prescription Refresh") + viewModel.refreshPrescriptions() } } } @@ -463,7 +484,10 @@ fun DebugScreenMain( DebugCard( title = "Card Wall" ) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "Fake NFC Capability", modifier = Modifier @@ -520,7 +544,6 @@ fun DebugScreenMain( item { FeatureToggles(viewModel = viewModel) } - item { RotatingLog(viewModel = viewModel) } @@ -534,8 +557,10 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV DebugCard(modifier, title = "Log") { val context = LocalContext.current val mailAddress = stringResource(R.string.settings_contact_mail_address) - Button( + LabelButton( modifier = Modifier.fillMaxWidth(), + icon = painterResource(R.drawable.ic_log_mail), + text = "Send mail", onClick = { val intent = Intent(Intent.ACTION_SENDTO) intent.data = Uri.parse("mailto:") @@ -558,9 +583,7 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV context.startActivity(intent) } } - ) { - Text("Send Mail") - } + ) } } @@ -639,7 +662,11 @@ private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSet enabled = !virtualHealthCardLoading ) { if (virtualHealthCardLoading) { - CircularProgressIndicator(Modifier.size(24.dp), strokeWidth = 2.dp, color = AppTheme.colors.neutral600) + CircularProgressIndicator( + Modifier.size(24.dp), + strokeWidth = 2.dp, + color = AppTheme.colors.neutral600 + ) SpacerSmall() } Text("Set Virtual Health Card for Active Profile", textAlign = TextAlign.Center) @@ -706,7 +733,10 @@ fun EnvironmentSelector( } ) { Row( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Small), + modifier = Modifier.padding( + horizontal = PaddingDefaults.Medium, + vertical = PaddingDefaults.Small + ), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt similarity index 95% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt index ee454dc3..76c0e894 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugScreenWrapper.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import androidx.compose.runtime.Composable import androidx.navigation.NavController diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt similarity index 99% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt index c4e1561a..40be1a3e 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/ui/DebugSettingsViewModel.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import android.content.Intent import android.content.pm.PackageManager @@ -34,8 +34,8 @@ import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.ErezeptApp import de.gematik.ti.erp.app.VisibleDebugTree import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase -import de.gematik.ti.erp.app.debug.data.DebugSettingsData -import de.gematik.ti.erp.app.debug.data.Environment +import de.gematik.ti.erp.app.data.DebugSettingsData +import de.gematik.ti.erp.app.data.Environment import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager import de.gematik.ti.erp.app.featuretoggle.Features diff --git a/app/features/src/main/AndroidManifest.xml b/app/features/src/main/AndroidManifest.xml index 021d37c1..8cf395e1 100644 --- a/app/features/src/main/AndroidManifest.xml +++ b/app/features/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ + { - debugOverrides() - fullContainerTreeOnError = true - } - } - bindProvider { ExternalAuthenticatorListViewModel(instance(), instance()) } - bindProvider { CheckVersionUseCase(instance(), instance()) } - } - - private val checkVersionUseCase: CheckVersionUseCase by instance() - - private val auth: AuthenticationUseCase by instance() - - private val analytics: Analytics by instance() - - private val intentHandler = IntentHandler(this) - - private val _nfcTag = MutableSharedFlow() - val nfcTagFlow: Flow - get() = _nfcTag.onStart { - if (!NfcAdapter.getDefaultAdapter(this@MainActivity).isEnabled) { - throw NfcNotEnabledException() - } - } - - private val authenticationModeAndMethod: Flow - get() = auth.authenticationModeAndMethod +class MainActivity : BaseActivity() { // @VisibleForTesting(otherwise = VisibleForTesting.NONE) // Only visible for testing, otherwise shows a warning val testWrapper: TestWrapper by instance() @@ -145,6 +90,7 @@ class MainActivity : DemoModeActivity(), DIAware { // @RestrictTo(RestrictTo.Scope.TESTS) val elementsUsedInTests: SnapshotStateMap = mutableStateMapOf() + @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -172,6 +118,7 @@ class MainActivity : DemoModeActivity(), DIAware { if (!BuildConfig.DEBUG) { installMessageConversionExceptionHandler() } + WindowCompat.setDecorFitsSystemWindows(window, false) setContent { @@ -179,6 +126,7 @@ class MainActivity : DemoModeActivity(), DIAware { LaunchedEffect(view) { ViewCompat.setWindowInsetsAnimationCallback(view, null) } + withDI(di) { CompositionLocalProvider( LocalActivity provides this, @@ -187,16 +135,17 @@ class MainActivity : DemoModeActivity(), DIAware { LocalAuthenticator provides rememberAuthenticator(intentHandler) ) { val authenticator = LocalAuthenticator.current - AppContent { settingsController -> + AppContent { val profilesController = rememberProfilesController() - val screenShotState by settingsController.screenshotState + val mainScreenController = rememberMainScreenController() + val screenshotsAllowed by mainScreenController.screenshotsState - if (screenShotState.screenshotsAllowed) { + if (screenshotsAllowed) { this.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } else { this.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } - val auth by produceState(null) { + val authentication by produceState(null) { launch { authenticationModeAndMethod.distinctUntilChangedBy { it::class } .collect { @@ -212,12 +161,9 @@ class MainActivity : DemoModeActivity(), DIAware { val navController = rememberNavController() val noDrawModifier = Modifier.graphicsLayer(alpha = 0f) val activeProfile by profilesController.getActiveProfileState() - val ssoTokenValid = rememberSaveable(activeProfile.ssoTokenScope) { - activeProfile.ssoTokenValid() - } Box { - if (auth !is AuthenticationModeAndMethod.Authenticated) { + if (authentication !is Authenticated) { Image( painterResource(R.drawable.erp_logo), null, @@ -227,17 +173,16 @@ class MainActivity : DemoModeActivity(), DIAware { DialogHost { Box( - if (auth is AuthenticationModeAndMethod.Authenticated) Modifier else noDrawModifier + if (authentication is Authenticated) Modifier else noDrawModifier ) { // mini card wall - if (!ssoTokenValid) { - HealthCardPrompt( - authenticator = authenticator.authenticatorHealthCard - ) - ExternalAuthPrompt( - authenticator = authenticator.authenticatorExternal - ) - } + HealthCardPrompt( + authenticator = authenticator.authenticatorHealthCard + ) + + ExternalAuthPrompt( + authenticator = authenticator.authenticatorExternal + ) SecureHardwarePrompt( authenticator = authenticator.authenticatorSecureElement @@ -256,7 +201,7 @@ class MainActivity : DemoModeActivity(), DIAware { DialogHost { AnimatedVisibility( - visible = auth is AuthenticationModeAndMethod.AuthenticationRequired, + visible = authentication is AuthenticationModeAndMethod.AuthenticationRequired, enter = fadeIn(), exit = fadeOut() ) { @@ -272,77 +217,4 @@ class MainActivity : DemoModeActivity(), DIAware { } } } - - @Requirement( - "O.Arch_10#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "If an update is required, the user is prompted to update via Google´s InAppUpdate function" - ) - private suspend fun checkAppUpdate() { - if (checkVersionUseCase.isUpdateRequired()) { - val appUpdateManager = AppUpdateManagerFactory.create(this) - val appUpdateInfoTask = appUpdateManager.appUpdateInfo - - appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> - if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { - val task = appUpdateManager.startUpdateFlow( - appUpdateInfo, - this, - AppUpdateOptions.defaultOptions(IMMEDIATE) - ) - - task.addOnCompleteListener { - if (task.isSuccessful && task.result != Activity.RESULT_OK) { - finish() - } - } - } - } - } - } - - override fun onUserInteraction() { - super.onUserInteraction() - - auth.resetInactivityTimer() - } - - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - - lifecycleScope.launch { - intent?.let { - intentHandler.propagateIntent(it) - } - } - } - - override fun onResume() { - super.onResume() - - NfcAdapter.getDefaultAdapter(applicationContext)?.let { - if (it.isEnabled) { - it.enableReaderMode( - this, - ::onTagDiscovered, - NfcAdapter.FLAG_READER_NFC_A - or NfcAdapter.FLAG_READER_NFC_B - or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, - Bundle() - ) - } - } - } - - private fun onTagDiscovered(tag: Tag) { - lifecycleScope.launch { - _nfcTag.emit(tag) - } - } - - override fun onPause() { - super.onPause() - - NfcAdapter.getDefaultAdapter(applicationContext)?.disableReaderMode(this) - } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt index 3c772ddf..2132e6a0 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt @@ -19,31 +19,36 @@ 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.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.core.content.edit import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.contentsquare.android.Contentsquare import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCaseData +import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase +import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.mainscreen.ui.MainScreenBottomSheetContentState import de.gematik.ti.erp.app.pharmacy.ui.PharmacySearchSheetContentState import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailBottomSheetContent import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine - -private const val PrefsName = "analyticsAllowed" +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch @Requirement( "A_19095", @@ -52,12 +57,20 @@ private const val PrefsName = "analyticsAllowed" ) class Analytics( private val context: Context, - private val prefs: SharedPreferences, - analyticsUseCase: AnalyticsUseCase + private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, + private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + private val analyticsUseCase: AnalyticsUseCase, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - private val _analyticsAllowed = MutableStateFlow(false) + + private val scope = CoroutineScope(dispatcher) + + private val isAnalyticsAllowed by lazy { + isAnalyticsAllowedUseCase.invoke() + } + val analyticsAllowed: StateFlow - get() = _analyticsAllowed + get() = isAnalyticsAllowed.stateIn(scope, SharingStarted.Eagerly, false) @Requirement( "A_19093", @@ -84,11 +97,9 @@ class Analytics( Contentsquare.forgetMe() - _analyticsAllowed.value = prefs.getBoolean(PrefsName, false) - if (_analyticsAllowed.value) { - allowAnalytics() - } else { - disallowAnalytics() + scope.launch { + val isAllowed = isAnalyticsAllowed.first() + setAnalyticsPreference(isAllowed) } } @@ -125,18 +136,20 @@ class Analytics( @Requirement( "O.Purp_5#5", sourceSpecification = "BSI-eRp-ePA", - rationale = "Enable usage analytics." + rationale = "Enable/disable usage analytics." ) - fun allowAnalytics() { - _analyticsAllowed.value = true - - Contentsquare.optIn(context) - - prefs.edit { - putBoolean(PrefsName, true) + fun setAnalyticsPreference(allow: Boolean) { + when (allow) { + true -> allowAnalytics() + else -> disallowAnalytics() } - - Napier.d("Analytics allowed") + } + private fun allowAnalytics() { + scope.launch { + changeAnalyticsStateUseCase.invoke(true) + } + Contentsquare.optIn(context) + Napier.i("Analytics allowed") } @Requirement( @@ -149,16 +162,12 @@ class Analytics( sourceSpecification = "BSI-eRp-ePA", rationale = "Disable usage analytics." ) - fun disallowAnalytics() { - _analyticsAllowed.value = false - - Contentsquare.optOut(context) - - prefs.edit { - putBoolean(PrefsName, false) + private fun disallowAnalytics() { + scope.launch { + changeAnalyticsStateUseCase.invoke(false) } - - Napier.d("Analytics disallowed") + Contentsquare.optOut(context) + Napier.i("Analytics disallowed") } fun trackIdentifiedWithIDP() { @@ -188,7 +197,7 @@ class Analytics( @Suppress("ComposableNaming") @Composable -fun trackNavigationChanges( +fun trackNavigationChangesAsync( navController: NavHostController, previousNavEntry: String, onNavEntryChange: (String) -> Unit diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt new file mode 100644 index 00000000..4f56a9b9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/mapper/ContentSquareMapper.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.analytics.mapper + +import de.gematik.ti.erp.app.navigation.NavigationRouteNames + +class ContentSquareMapper { + fun map(routeNames: NavigationRouteNames): String = + when (routeNames) { + NavigationRouteNames.DeviceCheckLoadingScreen -> "appsecurity:loading" + NavigationRouteNames.InsecureDeviceScreen -> "main_deviceSecurity" + NavigationRouteNames.IntegrityWarningScreen -> "main_integrityWarning" + NavigationRouteNames.ProfileDetailsScreen -> "" + NavigationRouteNames.ProfileColorAndImagePickerScreen -> "" + NavigationRouteNames.ProfileImageCropperScreen -> "" + NavigationRouteNames.ProfileTokenScreen -> "" + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt new file mode 100644 index 00000000..944f3b77 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/ChangeAnalyticsStateUseCase.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.analytics.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ChangeAnalyticsStateUseCase( + private val repository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(state: Boolean) { + withContext(dispatcher) { + repository.changeTrackingState(state) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt new file mode 100644 index 00000000..60e94a3e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/IsAnalyticsAllowedUseCase.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.analytics.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +class IsAnalyticsAllowedUseCase( + private val repository: SettingsRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow = repository.isAnalyticsAllowed().flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt new file mode 100644 index 00000000..25020e4d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecurityModule.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity + +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptDeviceSecurityRiskUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptIntegrityRiskUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.GetDeviceSecurityUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.IntegrityUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.IsIntegrityRiskAcceptedUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val appSecurityModule = DI.Module("appSecurityModule") { + bindSingleton { AppSecuritySession() } + bindProvider { IntegrityUseCase(instance()) } + bindProvider { GetDeviceSecurityUseCase(instance(), instance(), instance()) } + bindProvider { AcceptIntegrityRiskUseCase(instance(), instance()) } + bindProvider { AcceptDeviceSecurityRiskUseCase(instance(), instance()) } + bindProvider { IsIntegrityRiskAcceptedUseCase(instance(), instance()) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt new file mode 100644 index 00000000..96711ac9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/AppSecuritySession.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity + +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Holds the session variables which decides to show the integrity screen or device security screen in the app session + */ +class AppSecuritySession { + + private val isIntegrityAcceptedForSession = MutableStateFlow(false) + + private val isDeviceSecurityAcceptedForSession = MutableStateFlow(false) + + fun acceptIntegrityForSession() { + isIntegrityAcceptedForSession.value = true + } + + fun acceptDeviceSecurityForSession() { + isDeviceSecurityAcceptedForSession.value = true + } + + fun isIntegrityAcceptedForSession() = isIntegrityAcceptedForSession.value + + fun isDeviceSecurityAcceptedForSession() = isDeviceSecurityAcceptedForSession.value +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt new file mode 100644 index 00000000..509eb958 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityGraph.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.navigation + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.navigation +import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes.DeviceCheckLoadingScreen +import de.gematik.ti.erp.app.appsecurity.ui.DeviceCheckLoadingStartScreen +import de.gematik.ti.erp.app.appsecurity.ui.InsecureDeviceScreen +import de.gematik.ti.erp.app.appsecurity.ui.IntegrityWarningScreen +import de.gematik.ti.erp.app.navigation.renderComposable +import de.gematik.ti.erp.app.navigation.toNavigationString +import io.github.aakira.napier.Napier + +fun NavGraphBuilder.appSecurityGraph( + startDestination: String = DeviceCheckLoadingScreen.route, + navController: NavController, + onAppSecurityPassed: () -> Unit +) { + navigation(startDestination = startDestination, route = AppSecurityRoutes.subGraphName()) { + renderComposable( + route = DeviceCheckLoadingScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) + } + ) { + DeviceCheckLoadingStartScreen( + navController = navController, + navBackStackEntry = it, + onSecurityCheckResult = { appSecurityResult -> + Napier.d( + tag = "AppSecurity check", + message = "Results from DeviceCheckLoadingStartScreen\n" + + "isIntegrityAttested: ${appSecurityResult.isIntegritySecure}\n" + + "isDeviceSecure: ${appSecurityResult.isDeviceSecure}\n" + ) + when { + appSecurityResult.isIntegritySecure && appSecurityResult.isDeviceSecure -> onAppSecurityPassed() + appSecurityResult.isIntegritySecure && !appSecurityResult.isDeviceSecure -> + navController.navigate(AppSecurityRoutes.InsecureDeviceScreen.path()) { + launchSingleTop = true + } + + else -> { + navController.navigate( + route = AppSecurityRoutes.IntegrityWarningScreen.path( + appSecurityResult.toNavigationString() + ) + ) { + launchSingleTop = true + } + } + } + } + ) + } + renderComposable( + route = AppSecurityRoutes.IntegrityWarningScreen.route, + arguments = AppSecurityRoutes.IntegrityWarningScreen.arguments, + enterTransition = { + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) + } + ) { + Napier.d( + tag = "AppSecurity check", + message = "IntegrityWarningScreen loaded" + ) + IntegrityWarningScreen( + navController = navController, + navBackStackEntry = it, + onBack = { isDeviceSecure -> + if (isDeviceSecure) { + onAppSecurityPassed() + } else { + navController.navigate(AppSecurityRoutes.InsecureDeviceScreen.path()) { + launchSingleTop = true + } + } + } + ) + } + renderComposable( + route = AppSecurityRoutes.InsecureDeviceScreen.route, + enterTransition = { + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) + }, + exitTransition = { + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) + } + ) { + Napier.d( + tag = "AppSecurity check", + message = "InsecureDeviceScreen loaded" + ) + InsecureDeviceScreen( + navController = navController, + navBackStackEntry = it, + onBack = onAppSecurityPassed + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt new file mode 100644 index 00000000..02c20e6a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/navigation/AppSecurityRoutes.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.navigation + +import androidx.navigation.NavType +import androidx.navigation.navArgument +import de.gematik.ti.erp.app.navigation.NavigationRouteNames +import de.gematik.ti.erp.app.navigation.NavigationRoutes +import de.gematik.ti.erp.app.navigation.Routes + +object AppSecurityRoutes : NavigationRoutes { + + override fun subGraphName() = "app_security" + + const val IntegrityWarningScreenArgument = "isDeviceRiskAccepted" + + object DeviceCheckLoadingScreen : Routes(NavigationRouteNames.DeviceCheckLoadingScreen.name) + object InsecureDeviceScreen : Routes(NavigationRouteNames.InsecureDeviceScreen.name) + + object IntegrityWarningScreen : Routes( + path = NavigationRouteNames.IntegrityWarningScreen.name, + navArgument(IntegrityWarningScreenArgument) { type = NavType.StringType } + ) { + fun path(appSecurityResult: String) = + path(IntegrityWarningScreenArgument to appSecurityResult) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt new file mode 100644 index 00000000..deef855e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/AppSecurityController.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.appsecurity.usecase.GetDeviceSecurityUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.IntegrityUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.IsIntegrityRiskAcceptedUseCase +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import org.kodein.di.compose.rememberInstance + +class AppSecurityController( + private val integrityUseCase: IntegrityUseCase, + private val isIntegrityRiskAcceptedUseCase: IsIntegrityRiskAcceptedUseCase, + private val deviceSecurityUseCase: GetDeviceSecurityUseCase +) { + @Requirement( + "O.Arch_6#2", + "O.Resi_2#2", + "O.Resi_3#2", + "O.Resi_4#2", + "O.Resi_5#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check device integrity." + ) + suspend fun checkIntegrityRisk() = + combine( + integrityUseCase.runIntegrityAttestation(), + isIntegrityRiskAcceptedUseCase.invoke() + ) { isAttested, isRiskAccepted -> + Napier.d( + tag = "AppSecurity check", + message = "integrity check: isAttested $isAttested" + ) + Napier.d( + tag = "AppSecurity check", + message = "integrity check: isRiskAccepted $isRiskAccepted" + ) + return@combine when { + !isAttested && !isRiskAccepted -> false + else -> true + } + }.first() + + @Requirement( + "O.Plat_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for insecure Devices on first screen when the app is started." + ) + suspend fun checkDeviceSecurityRisk(): Boolean = deviceSecurityUseCase.invoke().first() +} + +@Composable +fun rememberAppSecurityController(): AppSecurityController { + val integrityUseCase: IntegrityUseCase by rememberInstance() + val isIntegrityRiskAcceptedUseCase: IsIntegrityRiskAcceptedUseCase by rememberInstance() + val deviceSecurityUseCase: GetDeviceSecurityUseCase by rememberInstance() + + return remember { + AppSecurityController( + integrityUseCase = integrityUseCase, + isIntegrityRiskAcceptedUseCase = isIntegrityRiskAcceptedUseCase, + deviceSecurityUseCase = deviceSecurityUseCase + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt new file mode 100644 index 00000000..c5bffe24 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/InsecureDeviceController.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptDeviceSecurityRiskUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptRiskEnum.AcceptForSession +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptRiskEnum.AcceptPermanent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class InsecureDeviceController( + private val acceptDeviceSecurityRiskUseCase: AcceptDeviceSecurityRiskUseCase, + private val scope: CoroutineScope +) { + fun acceptRiskForSession() { + scope.launch { + acceptDeviceSecurityRiskUseCase.invoke(acceptRiskEnum = AcceptForSession) + } + } + + fun acceptRiskPermanent() { + scope.launch { + acceptDeviceSecurityRiskUseCase.invoke(acceptRiskEnum = AcceptPermanent) + } + } +} + +@Composable +fun rememberInsecureDeviceController(): InsecureDeviceController { + val acceptDeviceSecurityRiskUseCase: AcceptDeviceSecurityRiskUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + return remember { + InsecureDeviceController( + acceptDeviceSecurityRiskUseCase = acceptDeviceSecurityRiskUseCase, + scope = scope + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt new file mode 100644 index 00000000..507b0d12 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/presentation/IntegrityWarningController.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptIntegrityRiskUseCase +import de.gematik.ti.erp.app.appsecurity.usecase.AcceptRiskEnum +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class IntegrityWarningController( + private val acceptIntegrityRiskUseCase: AcceptIntegrityRiskUseCase, + private val scope: CoroutineScope +) { + fun acceptRiskForSession() { + scope.launch { + acceptIntegrityRiskUseCase.invoke(acceptRiskEnum = AcceptRiskEnum.AcceptForSession) + } + } + + fun acceptRiskPermanent() { + scope.launch { + acceptIntegrityRiskUseCase.invoke(acceptRiskEnum = AcceptRiskEnum.AcceptPermanent) + } + } +} + +@Composable +fun rememberIntegrityWarningController(): IntegrityWarningController { + val acceptIntegrityRiskUseCase: AcceptIntegrityRiskUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + return remember { + IntegrityWarningController( + acceptIntegrityRiskUseCase = acceptIntegrityRiskUseCase, + scope = scope + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt new file mode 100644 index 00000000..670996a9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/DeviceCheckLoadingStartScreen.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.ui + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.appsecurity.presentation.rememberAppSecurityController +import de.gematik.ti.erp.app.appsecurity.ui.model.AppSecurityResult +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen + +private const val ANIMATION_TIME = 1000 + +class DeviceCheckLoadingStartScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val onSecurityCheckResult: (AppSecurityResult) -> Unit +) : Screen() { + + @Composable + override fun Content() { + val appSecurityController = rememberAppSecurityController() + + val infiniteTransition = rememberInfiniteTransition(label = "InfiniteTransition") + val angle by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(ANIMATION_TIME, easing = LinearEasing) + ), + label = "FloatAnimation" + ) + + LaunchedEffect(Unit) { + val isIntegritySecure = appSecurityController.checkIntegrityRisk() + val isDeviceSecure = appSecurityController.checkDeviceSecurityRisk() + // on obtaining results make a callback to use the results + onSecurityCheckResult( + AppSecurityResult( + isIntegritySecure = isIntegritySecure, + isDeviceSecure = isDeviceSecure + ) + ) + } + + Box(modifier = Modifier.fillMaxSize()) { + Image( + modifier = Modifier + .align(Alignment.Center) + .graphicsLayer { rotationZ = angle }, + painter = painterResource(R.drawable.erp_logo), + contentDescription = "integrity-check-loading" + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt new file mode 100644 index 00000000..5d6f35dd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/InsecureDeviceScreen.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.appsecurity.presentation.rememberInsecureDeviceController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.BottomAppBar +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.Toggle +import java.util.Locale + +@Requirement( + "O.Plat_1#4", + sourceSpecification = "BSI-eRp-ePA", + rationale = "insecure Devices warning." +) +class InsecureDeviceScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val onBack: () -> Unit +) : Screen() { + + @Composable + override fun Content() { + var checked by rememberSaveable { mutableStateOf(false) } + val scrollState = rememberScrollState() + + val insecureDeviceController = rememberInsecureDeviceController() + + AnimatedElevationScaffold( + elevated = scrollState.value > 0, + navigationMode = NavigationBarMode.Close, + bottomBar = { + BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { + Spacer(modifier = Modifier.weight(1f)) + Button( + onClick = { + if (checked) { + insecureDeviceController.acceptRiskPermanent() + } else { + insecureDeviceController.acceptRiskForSession() + } + onBack() + }, + shape = RoundedCornerShape(PaddingDefaults.Small) + ) { + Text(stringResource(R.string.ok).uppercase(Locale.getDefault())) + } + SpacerMedium() + } + }, + actions = {}, + topBarTitle = stringResource(id = R.string.insecure_device_title), + onBack = onBack + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(scrollState) + .padding(PaddingDefaults.Medium) + ) { + Image( + painterResource(id = R.drawable.laptop_woman_yellow), + null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxSize() + ) + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_header), + style = AppTheme.typography.h6 + ) + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_info), + style = AppTheme.typography.body1 + ) + Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) + Toggle( + checked = checked, + onCheckedChange = { checked = it }, + description = stringResource(id = R.string.insecure_device_accept) + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt new file mode 100644 index 00000000..9c046833 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/IntegrityWarningScreen.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes +import de.gematik.ti.erp.app.appsecurity.presentation.rememberIntegrityWarningController +import de.gematik.ti.erp.app.appsecurity.ui.model.AppSecurityResult +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.navigation.Screen +import de.gematik.ti.erp.app.navigation.fromNavigationString +import de.gematik.ti.erp.app.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.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.Toggle +import io.github.aakira.napier.Napier +import java.util.Locale + +@Requirement( + "O.Arch_6#3", + "O.Resi_2#3", + "O.Resi_3#3", + "O.Resi_4#3", + "O.Resi_5#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Show integrity warning." +) +@Requirement( + "A_21574", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Show integrity warning." +) +class IntegrityWarningScreen( + override val navController: NavController, + override val navBackStackEntry: NavBackStackEntry, + val onBack: (Boolean) -> Unit +) : Screen() { + @Composable + override fun Content() { + val result = + remember { + navBackStackEntry.arguments?.getString(AppSecurityRoutes.IntegrityWarningScreenArgument) ?: "" + } + + val appSecurityResult = fromNavigationString(result) + + Napier.d { "app security result $appSecurityResult" } + var checked by rememberSaveable { mutableStateOf(false) } + val scrollState = rememberScrollState() + val integrityWarningController = rememberIntegrityWarningController() + + AnimatedElevationScaffold( + elevated = scrollState.value > 0, + navigationMode = NavigationBarMode.Close, + bottomBar = { + BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { + Spacer(modifier = Modifier.weight(1f)) + Button( + onClick = { + if (checked) { + integrityWarningController.acceptRiskPermanent() + } else { + integrityWarningController.acceptRiskForSession() + } + onBack(appSecurityResult.isDeviceSecure) + }, + shape = RoundedCornerShape(PaddingDefaults.Small) + ) { + Text(stringResource(R.string.ok).uppercase(Locale.getDefault())) + } + SpacerMedium() + } + }, + actions = {}, + topBarTitle = stringResource(id = R.string.insecure_device_title_safetynet), + onBack = { + onBack(appSecurityResult.isDeviceSecure) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(scrollState) + .padding(PaddingDefaults.Medium) + ) { + Image( + painterResource(id = R.drawable.laptop_woman_pink), + null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxSize() + ) + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_header_safetynet), + style = AppTheme.typography.h6 + ) + SpacerSmall() + Text( + stringResource(id = R.string.insecure_device_info_safetynet), + style = AppTheme.typography.body1 + ) + // Todo wait for new uri from security staff +// val uriHandler = LocalUriHandler.current +// SpacerMedium() +// Text( +// stringResource(R.string.insecure_device_safetynet_more_info), +// style = AppTheme.typography.body2, +// color = AppTheme.colors.neutral600 +// ) +// SpacerSmall() +// val link = stringResource(R.string.insecure_device_safetynet_link) +// TextButton( +// modifier = Modifier.align(Alignment.End), +// onClick = { uriHandler.openUri(link) } +// ) { +// Text( +// stringResource(id = R.string.insecure_device_safetynet_link_text), +// style = AppTheme.typography.body2, +// color = AppTheme.colors.primary600 +// ) +// } + Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) + Toggle( + checked = checked, + onCheckedChange = { checked = it }, + description = stringResource(id = R.string.insecure_device_accept_safetynet) + ) + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt new file mode 100644 index 00000000..7f18c645 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/ui/model/AppSecurityResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.ui.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AppSecurityResult( + val isIntegritySecure: Boolean, + val isDeviceSecure: Boolean +) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt new file mode 100644 index 00000000..f908d7f7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptDeviceSecurityRiskUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.usecase + +import de.gematik.ti.erp.app.appsecurity.AppSecuritySession +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AcceptDeviceSecurityRiskUseCase( + private val repository: SettingsRepository, + private val session: AppSecuritySession, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(acceptRiskEnum: AcceptRiskEnum) { + withContext(dispatcher) { + when (acceptRiskEnum) { + AcceptRiskEnum.AcceptForSession -> session.acceptDeviceSecurityForSession() + AcceptRiskEnum.AcceptPermanent -> repository.acceptInsecureDevice() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt new file mode 100644 index 00000000..7746ee56 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptIntegrityRiskUseCase.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.usecase + +import de.gematik.ti.erp.app.appsecurity.AppSecuritySession +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AcceptIntegrityRiskUseCase( + private val repository: SettingsRepository, + private val session: AppSecuritySession, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(acceptRiskEnum: AcceptRiskEnum) { + withContext(dispatcher) { + when (acceptRiskEnum) { + AcceptRiskEnum.AcceptForSession -> session.acceptIntegrityForSession() + AcceptRiskEnum.AcceptPermanent -> repository.acceptIntegrityNotOk() + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt new file mode 100644 index 00000000..ac9af4bf --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/AcceptRiskEnum.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.usecase + +enum class AcceptRiskEnum { + AcceptForSession, AcceptPermanent +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt new file mode 100644 index 00000000..1bcbde95 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/GetDeviceSecurityUseCase.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.usecase + +import android.app.KeyguardManager +import android.content.Context +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.appsecurity.AppSecuritySession +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class GetDeviceSecurityUseCase( + private val context: Context, + private val settingsRepository: SettingsRepository, + private val appSecuritySession: AppSecuritySession, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + @Requirement( + "O.Plat_1#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for insecure Devices." + ) + operator fun invoke(): Flow = + settingsRepository.general + .map { + val isDeviceSecure = (context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) + .isDeviceSecure + + Napier.d( + tag = "AppSecurity check", + message = "Device security check: isDeviceSecure $isDeviceSecure" + ) + + val riskAccepted = + it.userHasAcceptedInsecureDevice || appSecuritySession.isDeviceSecurityAcceptedForSession() + + Napier.d( + tag = "AppSecurity check", + message = "Device security check: riskAccepted $riskAccepted" + ) + + val result = when { + !isDeviceSecure && !riskAccepted -> false + else -> true + } + Napier.d( + tag = "AppSecurity check", + message = "Device security check: result $result" + ) + result + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt similarity index 70% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt index c09aaec9..2fc86395 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IntegrityUseCase.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.attestation.usecase +package de.gematik.ti.erp.app.appsecurity.usecase import android.content.Context import android.util.Base64 @@ -24,9 +24,9 @@ import com.google.android.play.core.integrity.IntegrityManagerFactory import com.google.android.play.core.integrity.IntegrityTokenRequest import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.Requirement - import de.gematik.ti.erp.app.secureRandomInstance import de.gematik.ti.erp.app.vau.toLowerCaseHex +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow @@ -41,7 +41,12 @@ import java.security.spec.X509EncodedKeySpec import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec -private const val RequiredSaltLength = 32 +private const val REQUIRED_SALT_LENGTH = 32 +private const val DEVICE_INTEGRITY = "deviceIntegrity" +private const val MEETS_DEVICE_INTEGRITY = "MEETS_DEVICE_INTEGRITY" +private const val SHA_256 = "SHA-256" +private const val AES = "AES" +private const val EC = "EC" class IntegrityUseCase( private val context: Context @@ -54,8 +59,8 @@ class IntegrityUseCase( "O.Resi_5#1", sourceSpecification = "BSI-eRp-ePA", rationale = "In the release process, the app is signed and the signed app bundle is uploaded to the store. " + - "An altered application can only run on a jailbroken device. We are using Googles Integrity API " + - "to detect jailbreaked devices. If a user is using a jailbroken device, may it be known or unknown, " + + "An altered application can only run on a jail-broken device. We are using Googles Integrity API " + + "to detect jail-broken devices. If a user is using a jail-broken device, may it be known or unknown, " + "we display a security alert so a user can make an informed decision to use or " + "not use the application.." ) @@ -63,8 +68,7 @@ class IntegrityUseCase( val salt = provideSalt() val ourNonce = generateNonce(salt, nonceData()) - val integrityManager = - IntegrityManagerFactory.create(context) + val integrityManager = IntegrityManagerFactory.create(context) val tokenResponse = integrityManager.requestIntegrityToken( IntegrityTokenRequest.builder() @@ -81,45 +85,39 @@ class IntegrityUseCase( sourceSpecification = "BSI-eRp-ePA", rationale = "Signature via ecdh ephemeral-static (one time usage)" ) - val decryptionKeyBytes: ByteArray = - Base64.decode(BuildKonfig.INTEGRITY_API_KEY, Base64.DEFAULT) + val decryptionKeyBytes: ByteArray = Base64.decode(BuildKonfig.INTEGRITY_API_KEY, Base64.DEFAULT) - val decryptionKey: SecretKey = SecretKeySpec( - decryptionKeyBytes, - "AES" - ) + val decryptionKey: SecretKey = SecretKeySpec(decryptionKeyBytes, AES) - val encodedVerificationKey: ByteArray = - Base64.decode(BuildKonfig.INTEGRITY_VERIFICATION_KEY, Base64.DEFAULT) + val encodedVerificationKey: ByteArray = Base64.decode(BuildKonfig.INTEGRITY_VERIFICATION_KEY, Base64.DEFAULT) - val verificationKey: PublicKey = KeyFactory.getInstance("EC") + val verificationKey: PublicKey = KeyFactory.getInstance(EC) .generatePublic(X509EncodedKeySpec(encodedVerificationKey)) - val jwe: JsonWebEncryption = - JsonWebStructure.fromCompactSerialization(token) as JsonWebEncryption + val jwe: JsonWebEncryption = JsonWebStructure.fromCompactSerialization(token) as JsonWebEncryption jwe.key = decryptionKey val compactJws = jwe.payload val jws = JsonWebStructure.fromCompactSerialization(compactJws) + jws.key = verificationKey - val requestDetails = JSONObject(jws.payload).getJSONObject("deviceIntegrity") + val requestDetails = JSONObject(jws.payload).getJSONObject(DEVICE_INTEGRITY) - emit(requestDetails.toString().contains("MEETS_DEVICE_INTEGRITY")) + emit(requestDetails.toString().contains(MEETS_DEVICE_INTEGRITY)) }.catch { + Napier.e { "integrity check not done" } emit(true) } private fun nonceData() = ("GmtkEPrescriptionApp: " + System.currentTimeMillis()).toByteArray() - private fun provideSalt() = ByteArray(RequiredSaltLength).apply { + private fun provideSalt() = ByteArray(REQUIRED_SALT_LENGTH).apply { secureRandomInstance().nextBytes(this) } - private fun generateNonce(salt: ByteArray, word: ByteArray): String { - val combined = word + salt - return MessageDigest.getInstance("SHA-256").digest(combined).toLowerCaseHex().decodeToString() - } + private fun generateNonce(salt: ByteArray, word: ByteArray): String = + MessageDigest.getInstance(SHA_256).digest((word + salt)).toLowerCaseHex().decodeToString() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt new file mode 100644 index 00000000..3c217393 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/appsecurity/usecase/IsIntegrityRiskAcceptedUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.appsecurity.usecase + +import de.gematik.ti.erp.app.appsecurity.AppSecuritySession +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class IsIntegrityRiskAcceptedUseCase( + private val repository: SettingsRepository, + private val session: AppSecuritySession, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow = + repository.general.map { + it.userHasAcceptedIntegrityNotOk || session.isIntegrityAcceptedForSession() + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt new file mode 100644 index 00000000..1433f219 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/base/BaseActivity.kt @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("LongMethod", "MagicNumber") + +package de.gematik.ti.erp.app.base + +import android.app.Activity +import android.app.Dialog +import android.content.Intent +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.Gravity +import android.widget.FrameLayout +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ContextThemeWrapper +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.setMargins +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.google.android.material.snackbar.Snackbar +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.UpdateAvailability +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.NfcNotEnabledException +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.apicheck.usecase.CheckVersionUseCase +import de.gematik.ti.erp.app.cardwall.ui.ExternalAuthenticatorListViewModel +import de.gematik.ti.erp.app.core.IntentHandler +import de.gematik.ti.erp.app.debugOverrides +import de.gematik.ti.erp.app.demomode.DefaultDemoModeObserver +import de.gematik.ti.erp.app.demomode.DemoModeObserver +import de.gematik.ti.erp.app.demomode.di.demoModeModule +import de.gematik.ti.erp.app.demomode.di.demoModeOverrides +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.timeouts.usecase.GetPauseMetricUseCase +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.utils.extensions.DialogScaffold +import de.gematik.ti.erp.app.utils.extensions.SnackbarScaffold +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.Copy +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.android.retainedSubDI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +open class BaseActivity : + SnackbarScaffold, + DialogScaffold, + DIAware, + AppCompatActivity(), + DemoModeObserver by DefaultDemoModeObserver() { + + override val di by retainedSubDI(closestDI(), copy = Copy.All) { + // should be only done from feature module + import(demoModeModule) + if (isDemoMode()) demoModeOverrides() + when { + BuildConfig.DEBUG && BuildKonfig.INTERNAL -> { + debugOverrides() + fullContainerTreeOnError = true + } + } + bindProvider { ExternalAuthenticatorListViewModel(instance(), instance()) } + bindProvider { CheckVersionUseCase(instance(), instance()) } + } + + private val checkVersionUseCase: CheckVersionUseCase by instance() + + private val pauseTimeoutUseCase: GetPauseMetricUseCase by instance() + + private val inactivityTimeoutObserver: InactivityTimeoutObserver by instance() + + val analytics: Analytics by instance() + + val intentHandler = IntentHandler(this@BaseActivity) + + // This is needed to be declared here so that the dialog can be cancelled on-pause + private var dialog: Dialog? = null + + private val _nfcTag = MutableSharedFlow() + + private var pauseTimerHandler: Handler = Handler(Looper.getMainLooper()) + + /** + * A [Runnable] that makes the app require an authentication + */ + @Requirement( + "O.Auth_7", + "O.Plat_12", + sourceSpecification = "BSI-eRp-ePA", + rationale = "The LifeCycleState of the app is monitored. If the app stopped, the authentication " + + "process starts after a delay of 30 seconds. We opted for this delay for usability reasons, " + + "as selecting the profile picture and external authentication requires pausing the app." + ) + private val pauseTimerRunnable = Runnable { + inactivityTimeoutObserver.forceRequireAuthentication() + } + + override fun onPause() { + super.onPause() + // cancel the dialog when pausing since it might show up security concerns + dialog?.cancel() + + // this sets the app to require authentication after the pause-timeout + val pauseTimeout = pauseTimeoutUseCase.invoke() + Napier.i { "Started pause timer for ${pauseTimeout.inWholeMilliseconds}" } + pauseTimerHandler.postDelayed(pauseTimerRunnable, pauseTimeout.inWholeMilliseconds) + + NfcAdapter.getDefaultAdapter(applicationContext)?.disableReaderMode(this) + } + + override fun onUserInteraction() { + super.onUserInteraction() + inactivityTimeoutObserver.resetInactivityTimer() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + lifecycleScope.launch { + intent?.let { + intentHandler.propagateIntent(it) + } + } + } + + override fun onResume() { + super.onResume() + Napier.i { "Stopped pause timer" } + pauseTimerHandler.removeCallbacks(pauseTimerRunnable) + + NfcAdapter.getDefaultAdapter(applicationContext)?.let { + if (it.isEnabled) { + it.enableReaderMode( + this, + ::onTagDiscovered, + NfcAdapter.FLAG_READER_NFC_A + or NfcAdapter.FLAG_READER_NFC_B + or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, + Bundle() + ) + } + } + } + + override fun show(content: @Composable (Dialog) -> Unit) { + dialog = Dialog(this, R.style.ThemeOverlay_MaterialAlertDialog_Rounded) + dialog?.window?.decorView?.let { + it.setViewTreeLifecycleOwner(this) + it.setViewTreeSavedStateRegistryOwner(this) + it.setViewTreeOnBackPressedDispatcherOwner(this) + it.setViewTreeViewModelStoreOwner(this) + } + val composableView = ComposeView(this) + composableView.let { + it.setViewTreeViewModelStoreOwner(this) + it.setViewTreeLifecycleOwner(this) + it.setViewTreeSavedStateRegistryOwner(this) + it.setViewTreeOnBackPressedDispatcherOwner(this) + dialog?.let { dialogNotNull -> + it.setContent { + AppTheme { + content(dialogNotNull) + } + } + } + } + dialog?.setContentView(composableView) + dialog?.setCancelable(true) + dialog?.show() + } + + override fun show(text: String, icon: Int, backgroundTint: Int) { + val snackbar = Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT) + val theme = ContextThemeWrapper( + applicationContext, + R.style.ThemeOverlay_MaterialAlertDialog_Rounded + ).theme + snackbar.setBackgroundTint(resources.getColor(backgroundTint, theme)) + snackbar.view.updateLayoutParams { + width = CoordinatorLayout.LayoutParams.MATCH_PARENT + height = CoordinatorLayout.LayoutParams.WRAP_CONTENT + gravity = Gravity.BOTTOM + setMargins(24) + } + snackbar.show() + } + + private fun onTagDiscovered(tag: Tag) { + lifecycleScope.launch { + _nfcTag.emit(tag) + } + } + + val authenticationModeAndMethod: StateFlow + get() = inactivityTimeoutObserver.authenticationModeAndMethod + .stateIn(lifecycleScope, SharingStarted.Eagerly, AuthenticationModeAndMethod.None) + + @Requirement( + "O.Arch_10#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "If an update is required, the user is prompted to update via Google´s InAppUpdate function" + ) + suspend fun checkAppUpdate() { + if (checkVersionUseCase.isUpdateRequired()) { + val appUpdateManager = AppUpdateManagerFactory.create(this) + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { + val task = appUpdateManager.startUpdateFlow( + appUpdateInfo, + this, + AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE) + ) + + task.addOnCompleteListener { + if (task.isSuccessful && task.result != Activity.RESULT_OK) { + finish() + } + } + } + } + } + } + + val nfcTagFlow: Flow + get() = _nfcTag.onStart { + if (!NfcAdapter.getDefaultAdapter(this@BaseActivity).isEnabled) { + throw NfcNotEnabledException() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt index 3487406d..ee60af26 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt @@ -18,14 +18,14 @@ package de.gematik.ti.erp.app.cardunlock.model -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes object UnlockEgkNavigation { - object Intro : Route("healthCardPassword_introduction") - object CardAccessNumber : Route("healthCardPassword_can") - object PersonalUnblockingKey : Route("healthCardPassword_puk") - object OldSecret : Route("healthCardPassword_oldPin") - object NewSecret : Route("healthCardPassword_pin") - object UnlockEgk : Route("healthCardPassword_readCard") - object TroubleShooting : Route("troubleShooting") + object Intro : Routes("healthCardPassword_introduction") + object CardAccessNumber : Routes("healthCardPassword_can") + object PersonalUnblockingKey : Routes("healthCardPassword_puk") + object OldSecret : Routes("healthCardPassword_oldPin") + object NewSecret : Routes("healthCardPassword_pin") + object UnlockEgk : Routes("healthCardPassword_readCard") + object TroubleShooting : Routes("troubleShooting") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt index b6421f0a..b37fac11 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt @@ -45,7 +45,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.model.UnlockEgkNavigation import de.gematik.ti.erp.app.cardwall.ui.CardAccessNumber @@ -97,7 +97,7 @@ fun UnlockEgKScreen( val unlockNavController = rememberNavController() var previousNavEntry by remember { mutableStateOf("healthCardPassword_introduction") } - trackNavigationChanges(unlockNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(unlockNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) var cardAccessNumber by rememberSaveable { mutableStateOf("") } var personalUnblockingKey by rememberSaveable { mutableStateOf("") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt index e300652f..17d05ee2 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt @@ -80,7 +80,7 @@ import androidx.navigation.navOptions import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.domain.biometric.deviceStrongBiometricStatus @@ -159,7 +159,7 @@ fun CardWallScreen( } var previousNavEntry by remember { mutableStateOf("cardwall_introduction") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) var cardAccessNumber by rememberSaveable { mutableStateOf("") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNavigation.kt similarity index 54% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNavigation.kt index 055a765a..b85f5823 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNavigation.kt @@ -18,17 +18,17 @@ package de.gematik.ti.erp.app.cardwall.ui.model -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes object CardWallNavigation { - object Troubleshooting : Route("troubleShooting") - object ExternalAuthenticator : Route("cardWall_extAuth") - object Intro : Route("cardWall_introduction") - object CardAccessNumber : Route("cardWall_CAN") - object PersonalIdentificationNumber : Route("cardWall_PIN") - object AuthenticationSelection : Route("cardWall_saveLogin") - object AlternativeOption : Route("cardWall_saveLoginSecurityInfo") - object Authentication : Route("cardWall_readCard") - object OrderHealthCard : Route("contactInsuranceCompany") - object UnlockEgk : Route("healthCardPassword_introduction") + object Troubleshooting : Routes("troubleShooting") + object ExternalAuthenticator : Routes("cardWall_extAuth") + object Intro : Routes("cardWall_introduction") + object CardAccessNumber : Routes("cardWall_CAN") + object PersonalIdentificationNumber : Routes("cardWall_PIN") + object AuthenticationSelection : Routes("cardWall_saveLogin") + object AlternativeOption : Routes("cardWall_saveLoginSecurityInfo") + object Authentication : Routes("cardWall_readCard") + object OrderHealthCard : Routes("contactInsuranceCompany") + object UnlockEgk : Routes("healthCardPassword_introduction") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt index 1f665687..bd6599b1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt @@ -63,7 +63,6 @@ import de.gematik.ti.erp.app.demomode.startAppWithNormalMode import de.gematik.ti.erp.app.demomode.ui.DemoModeStatusBar import de.gematik.ti.erp.app.demomode.ui.checkForDemoMode import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import de.gematik.ti.erp.app.theme.AppTheme import kotlinx.coroutines.Job @@ -85,7 +84,7 @@ val LocalAnalytics = @Composable fun AppContent( - content: @Composable (settingsController: SettingsController) -> Unit + content: @Composable () -> Unit ) { val settingsController = rememberSettingsController() val zoomState by settingsController.zoomState @@ -111,7 +110,7 @@ fun AppContent( }, appContent = { Box(modifier = Modifier.zoomable(enabled = zoomState.zoomEnabled)) { - content(settingsController) + content() } } ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt new file mode 100644 index 00000000..23c60611 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/DebugTimeoutScreen.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("MagicNumber", "LongMethod") + +package de.gematik.ti.erp.app.debugsettings.timeout + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.debugsettings.timeout.intent.restartApp +import de.gematik.ti.erp.app.debugsettings.timeout.ui.MetricChangeDialog +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.NoError +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsScreenViewModel +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.SpacerXXLargeMedium +import de.gematik.ti.erp.app.utils.extensions.LocalDialog +import de.gematik.ti.erp.app.utils.extensions.LocalSnackbar +import org.kodein.di.compose.rememberInstance + +object DebugTimeoutScreen { + @Composable + fun Content( + onBack: () -> Unit + ) { + val viewmodel by rememberInstance() + val dialog = LocalDialog.current + val snackbar = LocalSnackbar.current + val activity = LocalActivity.current + + val scrollState = rememberScrollState() + val elevated by remember { derivedStateOf { scrollState.value > 0 } } + + val inactivityMetric by viewmodel.inactivityMetricDuration.collectAsStateWithLifecycle() + val pauseMetric by viewmodel.pauseMetricDuration.collectAsStateWithLifecycle() + val error by viewmodel.error.collectAsStateWithLifecycle(NoError) + + AnimatedElevationScaffold( + topBarTitle = "Biometric settings", + actions = {}, + elevated = elevated, + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .padding(horizontal = PaddingDefaults.Medium) + ) { + SpacerMedium() + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.weight(0.4f), + text = "Inactivity timer:", + style = MaterialTheme.typography.body1 + ) + Card( + modifier = Modifier.weight(0.6f), + border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + ) { + TextButton( + onClick = { + dialog.show { dialog -> + MetricChangeDialog( + label = "Inactivity Timer", + currentValue = inactivityMetric, + onDismissRequest = { + dialog.dismiss() + }, + onValueChanged = { value, duration -> + viewmodel.setInactivityMetric(value, duration) + dialog.dismiss() + snackbar.show("Inactivity timer reset. Restart required") + } + ) + } + } + ) { + Text( + "$inactivityMetric", + style = MaterialTheme.typography.body1 + ) + } + } + } + SpacerMedium() + Text( + text = "The inactivity timer is called when the app is running but nothing is clicked", + style = MaterialTheme.typography.subtitle2 + ) + SpacerLarge() + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + modifier = Modifier.weight(0.4f), + text = "Pause Timer:", + style = MaterialTheme.typography.body1 + ) + Card( + modifier = Modifier.weight(0.6f), + border = BorderStroke(0.5.dp, AppTheme.colors.primary600) + ) { + TextButton( + onClick = { + dialog.show { dialog -> + MetricChangeDialog( + label = "Pause Timer", + currentValue = pauseMetric, + onDismissRequest = { + dialog.dismiss() + }, + onValueChanged = { value, duration -> + viewmodel.setPauseMetric(value, duration) + dialog.dismiss() + snackbar.show("Pause timer reset. Restart required") + } + ) + } + } + ) { + Text( + "$pauseMetric", + style = MaterialTheme.typography.body1 + ) + } + } + } + SpacerMedium() + Text( + text = "The pause timer is called when the app is minimized", + style = MaterialTheme.typography.subtitle2 + ) + SpacerXXLargeMedium() + TextButton( + modifier = Modifier.padding(horizontal = PaddingDefaults.XXLargeMedium), + border = BorderStroke(1.dp, AppTheme.colors.neutral300), + onClick = { + viewmodel.resetToDefaultMetrics() + snackbar.show("All timers reset. Restart required") + } + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_reset), + contentDescription = null + ) + Text( + text = "Reset to Default", + style = MaterialTheme.typography.h6 + ) + } + } + SpacerXXLarge() + PrimaryButton( + modifier = Modifier + .padding(horizontal = PaddingDefaults.XXLargeMedium) + .fillMaxWidth(), + content = { + Text( + modifier = Modifier, + text = "Restart App", + style = MaterialTheme.typography.h6 + ) + }, + onClick = { + restartApp(activity) + } + ) + } + } + + // show errors for timeout + when (error) { + TimeoutsError.InactivityError -> snackbar.show( + text = "Error setting inactivity timer", + backgroundTint = R.color.red_500 + ) + + TimeoutsError.PauseError -> snackbar.show( + text = "Error setting pause timer", + backgroundTint = R.color.red_500 + ) + + TimeoutsError.Error -> snackbar.show( + text = "Error setting inactivity and pause timers", + backgroundTint = R.color.red_500 + + ) + + NoError -> { + // show nothing + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt new file mode 100644 index 00000000..a09ea33d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/intent/AppRestartIntent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.debugsettings.timeout.intent + +import android.content.Intent +import androidx.activity.ComponentActivity + +inline fun restartApp(activity: ComponentActivity) { + activity.finish() + activity.startActivity( + Intent(activity, T::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt new file mode 100644 index 00000000..3d0bcc00 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/debugsettings/timeout/ui/MetricChangeDialog.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.debugsettings.timeout.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Divider +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.HOURS +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.MINUTES +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum.SECONDS +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import kotlinx.coroutines.android.awaitFrame +import kotlin.time.Duration + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun MetricChangeDialog( + label: String, + currentValue: Duration, + onDismissRequest: () -> Unit, + onValueChanged: (String, DurationEnum) -> Unit +) { + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + + val (value, duration) = "$currentValue".extractTimeValues() + var textFieldValue by remember { mutableStateOf(TextFieldValue(value)) } + var expanded by remember { mutableStateOf(false) } + var selectedDurationEnum by remember { mutableStateOf(DurationEnum.valueOf(duration.name)) } + val items = listOf(SECONDS, MINUTES, HOURS) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + awaitFrame() + keyboard?.show() + } + + Surface { + Column( + modifier = Modifier + .padding(horizontal = PaddingDefaults.Medium) + .padding(vertical = PaddingDefaults.Large) + ) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + modifier = Modifier.weight(0.4f) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number + ), + keyboardActions = KeyboardActions( + onDone = { + onValueChanged(textFieldValue.text, selectedDurationEnum) + } + ), + label = { + Text( + text = label, + style = MaterialTheme.typography.body2 + ) + }, + value = textFieldValue, + onValueChange = { + textFieldValue = it + } + ) + Spacer(modifier = Modifier.weight(0.1f)) + Text( + modifier = Modifier.weight(0.25f), + text = "$selectedDurationEnum", + style = MaterialTheme.typography.body1 + ) + Box( + modifier = Modifier.weight(0.25f) + ) { + IconButton( + onClick = { + expanded = !expanded + } + ) { + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "$selectedDurationEnum" + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = !expanded + } + ) { + items.forEachIndexed { index, durationEnum -> + DropdownMenuItem( + onClick = { + keyboard?.show() + selectedDurationEnum = durationEnum + expanded = false + }, + content = { Text(text = durationEnum.name) } + ) + if (index + 1 < items.size) { + Divider() + } + } + } + } + } + SpacerMedium() + Row { + TextButton( + modifier = Modifier.weight(0.5f), + onClick = onDismissRequest + ) { + Text( + text = "Cancel", + style = MaterialTheme.typography.body1 + ) + } + TextButton( + modifier = Modifier.weight(0.5f), + onClick = { + onValueChanged(textFieldValue.text, selectedDurationEnum) + } + + ) { + Text( + text = "OK", + style = MaterialTheme.typography.body1 + ) + } + } + } + } +} + +private fun String.extractTimeValues(): Pair { + val regex = Regex("(\\d+)([hms])") + val matchResults = regex.findAll(this) + var value = "" + var unit = SECONDS + for (result in matchResults) { + value = result.groupValues[1] // Extract the numeric value + unit = DurationEnum.extractedUnit(result.groupValues[2]) // Extract the unit (h, m, or s) + } + return value to unit +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt index 5e5c9140..483d1fed 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt @@ -18,8 +18,9 @@ package de.gematik.ti.erp.app.di -import de.gematik.ti.erp.app.attestation.usecase.integrityModule +import de.gematik.ti.erp.app.appsecurity.appSecurityModule import de.gematik.ti.erp.app.authentication.di.authenticationModule +import de.gematik.ti.erp.app.timeouts.di.timeoutsSharedPrefsModule import de.gematik.ti.erp.app.cardunlock.cardUnlockModule import de.gematik.ti.erp.app.cardwall.cardWallModule import de.gematik.ti.erp.app.idp.idpModule @@ -43,10 +44,13 @@ import de.gematik.ti.erp.app.settings.settingsModule import de.gematik.ti.erp.app.vau.vauModule import org.kodein.di.DI +/** + * Use this only in the android-app module + */ val featureModule = DI.Module("featureModule", allowSilentOverride = true) { importAll( cardWallModule, - integrityModule, + appSecurityModule, networkModule, realmModule, idpModule, @@ -70,6 +74,7 @@ val featureModule = DI.Module("featureModule", allowSilentOverride = true) { pharmacyRepositoryModule, messageRepositoryModule, taskRepositoryModule, + timeoutsSharedPrefsModule, allowOverride = true ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt index e8c04bed..2c2e919d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt @@ -56,7 +56,7 @@ val idpModule = DI.Module("idpModule") { val idpUseCaseModule = DI.Module("idpUseCaseModule", allowSilentOverride = true) { bindProvider { IdpAlternateAuthenticationUseCase(instance(), instance(), instance()) } - bindProvider { + bindSingleton { DefaultIdpUseCase( repository = instance(), pairingRepository = instance(), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt index 56431e70..1254215a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt @@ -32,14 +32,12 @@ import de.gematik.ti.erp.app.orders.ui.OrderScreen import de.gematik.ti.erp.app.prescription.ui.PrescriptionsScreen import de.gematik.ti.erp.app.prescription.ui.rememberPrescriptionsController import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.settings.ui.SettingsScreen @Composable internal fun MainScreenContentNavHost( modifier: Modifier, mainScreenController: MainScreenController, - settingsController: SettingsController, mainNavController: NavController, bottomNavController: NavHostController, onElevateTopBar: (Boolean) -> Unit, @@ -91,8 +89,7 @@ internal fun MainScreenContentNavHost( MainNavigationScreens.Settings.arguments ) { SettingsScreen( - mainNavController = mainNavController, - settingsController = settingsController + mainNavController = mainNavController ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt index 9cdf8f9a..998db531 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt @@ -18,15 +18,12 @@ package de.gematik.ti.erp.app.mainscreen.navigation -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -34,18 +31,18 @@ import androidx.navigation.compose.composable import androidx.navigation.navOptions import de.gematik.ti.erp.app.LegalNoticeWithScaffold import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.analytics.trackPopUps +import de.gematik.ti.erp.app.appsecurity.navigation.AppSecurityRoutes +import de.gematik.ti.erp.app.appsecurity.navigation.appSecurityGraph import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.ui.CardWallScreen import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.debug.ui.DebugScreenWrapper -import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.license.ui.LicenseScreen import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.mainscreen.ui.InsecureDeviceScreen +import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController import de.gematik.ti.erp.app.mainscreen.ui.MainScreenScaffoldContainer import de.gematik.ti.erp.app.onboarding.ui.OnboardingNavigationScreens import de.gematik.ti.erp.app.onboarding.ui.OnboardingScreen @@ -68,16 +65,14 @@ import de.gematik.ti.erp.app.redeem.ui.RedeemNavigation import de.gematik.ti.erp.app.settings.ui.AllowAnalyticsScreen import de.gematik.ti.erp.app.settings.ui.PharmacyLicenseScreen import de.gematik.ti.erp.app.settings.ui.SecureAppWithPassword -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.rememberSettingsController +import de.gematik.ti.erp.app.ui.DebugScreenWrapper import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.navigationModeState import de.gematik.ti.erp.app.webview.URI_DATA_TERMS import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE import de.gematik.ti.erp.app.webview.WebViewScreen -@RequiresApi(Build.VERSION_CODES.O) @Requirement( "A_19178", sourceSpecification = "gemSpec_eRp_FdV", @@ -451,30 +446,42 @@ import de.gematik.ti.erp.app.webview.WebViewScreen fun MainScreenNavigation( navController: NavHostController ) { - val settingsController = rememberSettingsController() - val startDestination = checkFirstAppStart(settingsController) - LaunchedEffect(startDestination) { - // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots - // (on debug builds should be allowed for testing) - if (BuildConfig.DEBUG && startDestination == "onboarding") { - settingsController.onAllowScreenshots() - } - } + /** + * Main screen navigation start check + */ + val mainScreenController = rememberMainScreenController() + + val onboardingSucceeded = mainScreenController.onboardingSucceeded + + val startDestinationScreen = shouldOnboardingBeDone(onboardingSucceeded) + val analytics = LocalAnalytics.current val analyticsState by analytics.screenState + val mlKitAccepted by mainScreenController.mlKitAcceptedState trackPopUps(analytics, analyticsState) var previousNavEntry by remember { mutableStateOf("main") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) NavHost( navController, - startDestination = startDestination + startDestination = AppSecurityRoutes.subGraphName() ) { + appSecurityGraph(navController = navController) { + navController.navigate(startDestinationScreen) { + popUpTo(AppSecurityRoutes.DeviceCheckLoadingScreen.path()) { + inclusive = true + } + } + } composable(MainNavigationScreens.Onboarding.route) { - OnboardingScreen( - mainNavController = navController, - settingsController = settingsController - ) + OnboardingScreen { + navController.navigate(MainNavigationScreens.Prescriptions.path()) { + launchSingleTop = true + popUpTo(MainNavigationScreens.Onboarding.path()) { + inclusive = true + } + } + } } composable(MainNavigationScreens.DataProtection.route) { NavigationAnimation(mode = navigationMode) { @@ -496,8 +503,7 @@ fun MainScreenNavigation( MainNavigationScreens.Settings.arguments ) { SettingsScreen( - mainNavController = navController, - settingsController = settingsController + mainNavController = navController ) } composable(MainNavigationScreens.Camera.route) { @@ -511,8 +517,13 @@ fun MainScreenNavigation( ) MainScreenScaffoldContainer( mainNavController = navController, - onDeviceIsInsecure = { - navController.navigate(MainNavigationScreens.IntegrityNotOkScreen.path()) + mainScreenController = mainScreenController, + onClickAddPrescription = { + if (mlKitAccepted) { + navController.navigate(MainNavigationScreens.MlKitIntroScreen.path()) + } else { + navController.navigate(MainNavigationScreens.Camera.path()) + } } ) } @@ -536,26 +547,16 @@ fun MainScreenNavigation( } ) } - composable(MainNavigationScreens.InsecureDeviceScreen.route) { - @Requirement( - "O.Plat_1#4", - sourceSpecification = "BSI-eRp-ePA", - rationale = "insecure Devices warning." - ) - InsecureDeviceScreen( - stringResource(id = R.string.insecure_device_title), - painterResource(id = R.drawable.laptop_woman_yellow), - stringResource(id = R.string.insecure_device_header), - stringResource(id = R.string.insecure_device_info), - stringResource(id = R.string.insecure_device_accept) - ) { - navController.navigate(MainNavigationScreens.Prescriptions.route) - } - } composable(MainNavigationScreens.MlKitIntroScreen.route) { MlKitIntroScreen( - navController, - settingsController + onAcceptMLKit = { + mainScreenController.acceptMLKit() + navController.navigate(MainNavigationScreens.Camera.path()) + }, + onClickReadMore = { + navController.navigate(MainNavigationScreens.MlKitInformationScreen.path()) + }, + onBack = { navController.popBackStack() } ) } composable(MainNavigationScreens.MlKitInformationScreen.route) { @@ -563,18 +564,6 @@ fun MainScreenNavigation( navController ) } - composable(MainNavigationScreens.IntegrityNotOkScreen.route) { - InsecureDeviceScreen( - stringResource(id = R.string.insecure_device_title_safetynet), - painterResource(id = R.drawable.laptop_woman_pink), - stringResource(id = R.string.insecure_device_header_safetynet), - stringResource(id = R.string.insecure_device_info_safetynet), - stringResource(id = R.string.insecure_device_accept_safetynet), - pinUseCase = false - ) { - navController.navigate(MainNavigationScreens.Prescriptions.route) - } - } composable( MainNavigationScreens.Redeem.route ) { @@ -688,11 +677,7 @@ fun MainScreenNavigation( AllowAnalyticsScreen( onBack = { navController.popBackStack() }, onAllowAnalytics = { - if (it) { - settingsController.onTrackingAllowed() - } else { - settingsController.onTrackingDisallowed() - } + mainScreenController.allowAnalytics(it) } ) } @@ -700,8 +685,12 @@ fun MainScreenNavigation( composable(MainNavigationScreens.Password.route) { NavigationAnimation(mode = navigationMode) { SecureAppWithPassword( - navController, - settingsController + onSelectPasswordAsAuthenticationMode = { password -> + mainScreenController.selectPasswordAsAuthenticationMode(password) + }, + onBack = { + navController.popBackStack() + } ) } } @@ -789,11 +778,10 @@ fun MainScreenNavigation( } @Composable -private fun checkFirstAppStart(settingsController: SettingsController) = - if (settingsController.showOnboarding) { - MainNavigationScreens.Onboarding.route - } else { - MainNavigationScreens.Prescriptions.route +private fun shouldOnboardingBeDone(onboardingSucceeded: Boolean): String = + when (onboardingSucceeded) { + true -> MainNavigationScreens.Prescriptions.route + false -> MainNavigationScreens.Onboarding.route } @Composable diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt index 55b54ecb..25540d76 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt @@ -21,8 +21,8 @@ package de.gematik.ti.erp.app.mainscreen.navigation import android.os.Parcelable import androidx.navigation.NavType import androidx.navigation.navArgument -import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.prescription.detail.ui.model.PopUpName import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.parcelize.Parcelize @@ -33,22 +33,22 @@ import kotlinx.serialization.Serializable data class TaskIds(val ids: List) : Parcelable, List by ids object MainNavigationScreens { - object Onboarding : Route("onboarding") - object Settings : Route("settings") - object Camera : Route("main_scanner") - object Prescriptions : Route("main") - object Archive : Route("main_prescriptionArchive") + object Onboarding : Routes("onboarding") + object Settings : Routes("settings") + object Camera : Routes("main_scanner") + object Prescriptions : Routes("main") + object Archive : Routes("main_prescriptionArchive") object PrescriptionDetail : - Route( + Routes( "prescriptionDetail", navArgument("taskId") { type = NavType.StringType } ) { fun path(taskId: String) = path("taskId" to taskId) } - object Orders : Route("orders") + object Orders : Routes("orders") - object Messages : Route( + object Messages : Routes( "orders_detail", navArgument("orderId") { type = NavType.StringType } ) { @@ -56,43 +56,43 @@ object MainNavigationScreens { Messages.path("orderId" to orderId) } - object Pharmacies : Route("pharmacySearch") + object Pharmacies : Routes("pharmacySearch") - object Redeem : Route("redeem_methodSelection") + object Redeem : Routes("redeem_methodSelection") - object ProfileImageCropper : Route( + object ProfileImageCropper : Routes( "profile_editPicture_imageCropper", navArgument("profileId") { type = NavType.StringType } ) { fun path(profileId: String) = path("profileId" to profileId) } - object CardWall : Route( + object CardWall : Routes( "cardWall_introduction", navArgument("profileId") { type = NavType.StringType } ) { fun path(profileId: ProfileIdentifier) = path("profileId" to profileId) } - object InsecureDeviceScreen : Route("main_deviceSecurity") - object MlKitIntroScreen : Route("mlKit") - object MlKitInformationScreen : Route("mlKit_information") - object DataProtection : Route("settings_dataProtection") - object IntegrityNotOkScreen : Route("main_integrityWarning") + object MlKitIntroScreen : Routes("mlKit") + object MlKitInformationScreen : Routes("mlKit_information") + object DataProtection : Routes("settings_dataProtection") + object EditProfile : - Route("profile", navArgument("profileId") { type = NavType.StringType }) { + Routes("profile", navArgument("profileId") { type = NavType.StringType }) { fun path(profileId: String) = path("profileId" to profileId) } - object Terms : Route("settings_termsOfUse") - object Imprint : Route("settings_legalNotice") - object OpenSourceLicences : Route("settings_openSourceLicence") - object AdditionalLicences : Route("settings_additionalLicence") - object AllowAnalytics : Route("settings_productImprovements_complyTracking") - object Password : Route("settings_authenticationMethods_setAppPassword") - object Debug : Route("debug") - object OrderHealthCard : Route("contactInsuranceCompany") - - object UnlockEgk : Route( + + object Terms : Routes("settings_termsOfUse") + object Imprint : Routes("settings_legalNotice") + object OpenSourceLicences : Routes("settings_openSourceLicence") + object AdditionalLicences : Routes("settings_additionalLicence") + object AllowAnalytics : Routes("settings_productImprovements_complyTracking") + object Password : Routes("settings_authenticationMethods_setAppPassword") + object Debug : Routes("debug") + object OrderHealthCard : Routes("contactInsuranceCompany") + + object UnlockEgk : Routes( "healthCardPassword_introduction", navArgument("unlockMethod") { type = NavType.StringType } ) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt index b50b6d6f..35b2018a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt @@ -22,23 +22,43 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.attestation.usecase.IntegrityUseCase +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.orders.usecase.OrderUseCase import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase +import de.gematik.ti.erp.app.settings.usecase.AcceptMLKitUseCase +import de.gematik.ti.erp.app.settings.usecase.AllowAnalyticsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetCanStartToolTipsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase +import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase +import de.gematik.ti.erp.app.settings.usecase.SavePasswordUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveToolTippsShownUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance +@Suppress("LongParameterList") class MainScreenController( - private val integrityUseCase: IntegrityUseCase, private val messageUseCase: OrderUseCase, - private val settingsUseCase: SettingsUseCase + private val saveToolTipsShownUseCase: SaveToolTippsShownUseCase, + private val saveWelcomeDrawerShownUseCase: SaveWelcomeDrawerShownUseCase, + private val acceptMLKitUseCase: AcceptMLKitUseCase, + private val allowAnalyticsUseCase: AllowAnalyticsUseCase, + private val savePasswordUseCase: SavePasswordUseCase, + private val scope: CoroutineScope, + getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, + getOnboardingSucceededUseCase: GetOnboardingSucceededUseCase, + getCanStartToolTipsUseCase: GetCanStartToolTipsUseCase, + getShowWelcomeDrawerUseCase: GetShowWelcomeDrawerUseCase, + getMLKitAcceptedUseCase: GetMLKitAcceptedUseCase ) { enum class OrderedEvent { @@ -57,15 +77,39 @@ class MainScreenController( orderedEvent = null } - fun hasUnreadPrescriptionAvailable(profileIdentifier: ProfileIdentifier) = - messageUseCase.unreadPrescriptionAvailable(profileIdentifier) + private val screenshotsAllowed = + getScreenShotsAllowedUseCase.invoke() + + val screenshotsState + @Composable + get() = screenshotsAllowed.collectAsStateWithLifecycle(false) + + val onboardingSucceeded = getOnboardingSucceededUseCase.invoke() + + private val canStartToolTips = getCanStartToolTipsUseCase.invoke() + + val canStartToolTipsState + @Composable + get() = canStartToolTips.collectAsStateWithLifecycle(false) + + fun toolTippsShown() = scope.launch { + saveToolTipsShownUseCase() + } + + private val showWelcomeDrawer = + getShowWelcomeDrawerUseCase.invoke() + + val showWelcomeDrawerState + @Composable + get() = showWelcomeDrawer.collectAsStateWithLifecycle(false) + + fun welcomeDrawerShown() = scope.launch { + saveWelcomeDrawerShownUseCase() + } fun unreadOrders(profile: ProfilesUseCaseData.Profile) = messageUseCase.unreadOrders(profile) - fun unreadPrescriptionsInAllOrders(profileIdentifier: ProfileIdentifier) = - messageUseCase.unreadPrescriptionsInAllOrders(profileIdentifier) - suspend fun onRefresh(event: PrescriptionServiceState) { _onRefreshEvent.emit(event) } @@ -74,22 +118,60 @@ class MainScreenController( orderedEvent = if (hasError) OrderedEvent.Error else OrderedEvent.Success } - fun checkDeviceIntegrity() = integrityUseCase.runIntegrityAttestation().map { - !it && !settingsUseCase.general.first().userHasAcceptedInsecureDevice + private val mlKitAccepted = + getMLKitAcceptedUseCase.invoke() + + val mlKitAcceptedState + @Composable + get() = mlKitAccepted.collectAsStateWithLifecycle(false) + + fun acceptMLKit() = scope.launch { + acceptMLKitUseCase() + } + + @Requirement( + "O.Purp_5#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Enable usage analytics." + ) + fun allowAnalytics(allow: Boolean) = scope.launch { + allowAnalyticsUseCase(allow) + } + + fun selectPasswordAsAuthenticationMode(password: String) = scope.launch { + savePasswordUseCase.invoke(password) } } @Composable fun rememberMainScreenController(): MainScreenController { - val integrityUseCase by rememberInstance() val messageUseCase by rememberInstance() - val settingsUseCase by rememberInstance() + val getScreenShotsAllowedUseCase by rememberInstance() + val shouldShowOnboardingUseCase by rememberInstance() + val acceptMLKitUseCase by rememberInstance() + val getMLKitAcceptedUseCase by rememberInstance() + val allowAnalyticsUseCase by rememberInstance() + val savePasswordUseCase by rememberInstance() + val getShowToolTipsUseCase by rememberInstance() + val saveToolTipsShownUseCase by rememberInstance() + val getShowWelcomeDrawerUseCase by rememberInstance() + val saveWelcomeDrawerShownUseCase by rememberInstance() + val scope = rememberCoroutineScope() return remember { MainScreenController( - integrityUseCase = integrityUseCase, messageUseCase = messageUseCase, - settingsUseCase = settingsUseCase + getScreenShotsAllowedUseCase = getScreenShotsAllowedUseCase, + getOnboardingSucceededUseCase = shouldShowOnboardingUseCase, + acceptMLKitUseCase = acceptMLKitUseCase, + getMLKitAcceptedUseCase = getMLKitAcceptedUseCase, + allowAnalyticsUseCase = allowAnalyticsUseCase, + savePasswordUseCase = savePasswordUseCase, + getCanStartToolTipsUseCase = getShowToolTipsUseCase, + saveToolTipsShownUseCase = saveToolTipsShownUseCase, + getShowWelcomeDrawerUseCase = getShowWelcomeDrawerUseCase, + saveWelcomeDrawerShownUseCase = saveWelcomeDrawerShownUseCase, + scope = scope ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt deleted file mode 100644 index 36d4b6f9..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.Image -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.toggleable -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.Switch -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.ui.rememberSettingsController -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.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import kotlinx.coroutines.launch -import java.util.Locale -@Requirement( - "O.Arch_6#3", - "O.Resi_2#3", - "O.Resi_3#3", - "O.Resi_4#3", - "O.Resi_5#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Show integrity warning." -) -@Requirement( - "A_21574", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Show integrity warning." -) -@Composable -fun InsecureDeviceScreen( - headline: String, - icon: Painter, - headlineBody: String, - infoText: String, - toggleDescription: String, - pinUseCase: Boolean = true, - onBack: () -> Unit -) { - var checked by rememberSaveable { mutableStateOf(false) } - val scrollState = rememberScrollState() - val scope = rememberCoroutineScope() - - val settingsController = rememberSettingsController() - - AnimatedElevationScaffold( - elevated = scrollState.value > 0, - navigationMode = NavigationBarMode.Close, - bottomBar = { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(modifier = Modifier.weight(1f)) - Button( - onClick = { - if (checked) { - scope.launch { - settingsController.onAcceptInsecureDevice() - } - } - onBack() - }, - shape = RoundedCornerShape(PaddingDefaults.Small), - enabled = if (pinUseCase) true else checked - ) { - if (checked && pinUseCase) { - Text(stringResource(R.string.understand).uppercase(Locale.getDefault())) - } else { - Text(stringResource(R.string.ok).uppercase(Locale.getDefault())) - } - } - SpacerMedium() - } - }, - actions = {}, - topBarTitle = headline, - onBack = onBack - ) { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .verticalScroll(scrollState) - .padding(PaddingDefaults.Medium) - ) { - Image( - icon, - null, - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxSize() - ) - SpacerSmall() - Text( - headlineBody, - style = AppTheme.typography.h6 - ) - SpacerSmall() - Text( - infoText, - style = AppTheme.typography.body1 - ) - if (!pinUseCase) { - val uriHandler = LocalUriHandler.current - SpacerMedium() - Text( - stringResource(R.string.insecure_device_safetynet_more_info), - style = AppTheme.typography.body2, - color = AppTheme.colors.neutral600 - ) - SpacerSmall() - val link = stringResource(R.string.insecure_device_safetynet_link) - TextButton( - modifier = Modifier.align(Alignment.End), - onClick = { uriHandler.openUri(link) } - ) { - Text( - stringResource(id = R.string.insecure_device_safetynet_link_text), - style = AppTheme.typography.body2, - color = AppTheme.colors.primary600 - ) - } - } - Spacer(modifier = Modifier.height(PaddingDefaults.XXLarge)) - Toggle( - checked = checked, - onCheckedChange = { checked = it }, - description = toggleDescription - ) - } - } -} - -@Composable -private fun Toggle( - modifier: Modifier = Modifier, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - description: String -) { - Row( - modifier = modifier - .clip(RoundedCornerShape(16.dp)) - .toggleable( - value = checked, - onValueChange = onCheckedChange, - role = Role.Switch, - interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current - ) - .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(16.dp)) - .fillMaxWidth() - .padding(PaddingDefaults.Medium) - .semantics(true) {}, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - description, - style = AppTheme.typography.subtitle1, - modifier = Modifier.weight(1f) - ) - SpacerSmall() - Switch( - checked = checked, - onCheckedChange = null - ) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt index a0849658..6d2083af 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt @@ -47,13 +47,13 @@ import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenBottomPopUpNames +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.presentation.ProfilesController import de.gematik.ti.erp.app.profiles.ui.AvatarPicker import de.gematik.ti.erp.app.profiles.ui.ColorPicker import de.gematik.ti.erp.app.profiles.ui.ProfileImage import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.PrimaryButton @@ -89,12 +89,12 @@ sealed class MainScreenBottomSheetContentState { @Composable fun MainScreenBottomSheetContentState( mainNavController: NavController, + mainScreenController: MainScreenController, profilesController: ProfilesController, infoContentState: MainScreenBottomSheetContentState?, profileToRename: ProfilesUseCaseData.Profile, onCancel: () -> Unit ) { - val settingsController = rememberSettingsController() val profile by profilesController.getActiveProfileState() val title = when (infoContentState) { @@ -177,17 +177,13 @@ fun MainScreenBottomSheetContentState( is MainScreenBottomSheetContentState.Welcome -> ConnectBottomSheetContent( onClickConnect = { - scope.launch { - settingsController.welcomeDrawerShown() - } + mainScreenController.welcomeDrawerShown() mainNavController.navigate( MainNavigationScreens.CardWall.path(profile.id) ) }, onCancel = { - scope.launch { - settingsController.welcomeDrawerShown() - } + mainScreenController.welcomeDrawerShown() onCancel() } ) @@ -207,7 +203,7 @@ private fun EditProfileAvatar( onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit ) { ProfileColorAndImagePickerContent( - profile, + profile = profile, clearPersonalizedImage = clearPersonalizedImage, onPickPersonalizedImage = onPickPersonalizedImage, onSelectAvatar = onSelectAvatar, @@ -227,7 +223,10 @@ private fun ProfileColorAndImagePickerContent( Column(modifier = Modifier.fillMaxSize()) { SpacerMedium() ProfileImage(editableProfile) { - editableProfile = editableProfile.copy(image = null) + editableProfile = editableProfile.copy( + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) clearPersonalizedImage() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt index 31d798f7..114fdf49 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt @@ -40,19 +40,20 @@ import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenContentNavHost import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.profiles.presentation.ProfilesController import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.ui.SettingsController @Composable internal fun MainScreenScaffold( + modifier: Modifier = Modifier, mainScreenController: MainScreenController, - settingsController: SettingsController, profilesController: ProfilesController, mainNavController: NavController, bottomNavController: NavHostController, + showToolTipps: Boolean, tooltipBounds: MutableState>, onClickAddProfile: () -> Unit, onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, onClickAvatar: () -> Unit, + onClickAddPrescription: () -> Unit, scaffoldState: ScaffoldState ) { val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) @@ -66,19 +67,19 @@ internal fun MainScreenScaffold( var topBarElevated by remember { mutableStateOf(true) } Scaffold( - modifier = Modifier.testTag(TestTag.Main.MainScreen), + modifier = Modifier.testTag(TestTag.Main.MainScreen).then(modifier), topBar = { if (currentBottomNavigationRoute?.destination?.route != MainNavigationScreens.Settings.route) { MultiProfileTopAppBar( mainScreenController = mainScreenController, - settingsController = settingsController, profilesController = profilesController, - navController = mainNavController, isInPrescriptionScreen = isInPrescriptionScreen, + showToolTipps = showToolTipps, tooltipBounds = tooltipBounds, elevated = topBarElevated, onClickAddProfile = onClickAddProfile, - onClickChangeProfileName = onClickChangeProfileName + onClickChangeProfileName = onClickChangeProfileName, + onClickAddPrescription = onClickAddPrescription ) } }, @@ -106,7 +107,6 @@ internal fun MainScreenScaffold( MainScreenContentNavHost( modifier = Modifier.padding(innerPadding), mainScreenController = mainScreenController, - settingsController = settingsController, mainNavController = mainNavController, bottomNavController = bottomNavController, onClickAvatar = onClickAvatar, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt index 2bfe8954..a0d1885b 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt @@ -18,6 +18,8 @@ package de.gematik.ti.erp.app.mainscreen.ui +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.imePadding @@ -44,83 +46,73 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import androidx.navigation.navOptions -import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.trackMainScreenBottomPopUps -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController -import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController import de.gematik.ti.erp.app.profiles.presentation.ProfilesController.Companion.DEFAULT_EMPTY_PROFILE import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController -import de.gematik.ti.erp.app.settings.ui.rememberSettingsController -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterialApi::class) @Suppress("LongMethod") @Composable internal fun MainScreenScaffoldContainer( mainNavController: NavController, - onDeviceIsInsecure: () -> Unit + mainScreenController: MainScreenController, + onClickAddPrescription: () -> Unit ) { - val mainScreenController = rememberMainScreenController() - val settingsController = rememberSettingsController() - val profilesController = rememberProfilesController() - val context = LocalContext.current + + val showToolTips by mainScreenController.canStartToolTipsState + var startToolTips by remember { mutableStateOf(false) } + + val profilesController = rememberProfilesController() val bottomNavController = rememberNavController() + val scaffoldState = rememberScaffoldState() + + val showWelcomeDrawer by mainScreenController.showWelcomeDrawerState + + var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } + + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + skipHalfExpanded = true + ) + val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) + var previousNavEntry by remember { mutableStateOf("main") } - trackNavigationChanges(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + + trackNavigationChangesAsync(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + val isInPrescriptionScreen by remember { derivedStateOf { currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route } } - @Requirement( - "O.Plat_1#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for insecure Devices on MainScreen." - ) - CheckInsecureDevice(onDeviceIsInsecure) - @Requirement( - "O.Arch_6#2", - "O.Resi_2#2", - "O.Resi_3#2", - "O.Resi_4#2", - "O.Resi_5#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check device integrity." - ) - CheckDeviceIntegrity(mainScreenController, mainNavController) - val scaffoldState = rememberScaffoldState() - val scope = rememberCoroutineScope() + MainScreenSnackbar( mainScreenController = mainScreenController, scaffoldState = scaffoldState ) + OrderSuccessHandler(mainScreenController) - var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } - val sheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true - ) + if (sheetState.currentValue != ModalBottomSheetValue.Hidden) { DisposableEffect(Unit) { onDispose { - scope.launch { - settingsController.welcomeDrawerShown() + mainScreenController.welcomeDrawerShown() + if (showToolTips) { + startToolTips = true } } } } + LaunchedEffect(mainScreenBottomSheetContentState) { if (mainScreenBottomSheetContentState != null) { sheetState.show() @@ -140,22 +132,20 @@ internal fun MainScreenScaffoldContainer( trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) } } - LaunchedEffect(Unit) { - if (settingsController.showWelcomeDrawer.first()) { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() - } - } - LaunchedEffect(sheetState.isVisible) { - if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { - if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Welcome()) { - settingsController.welcomeDrawerShown() - } - mainScreenBottomSheetContentState = null - } + + if (showWelcomeDrawer) { + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() + mainScreenController.welcomeDrawerShown() } + LaunchedEffect(Unit) { - if (settingsController.talkbackEnabled(context)) { - settingsController.mainScreenTooltipsShown() + val accessibilityManager = + context.getSystemService(Context.ACCESSIBILITY_SERVICE) as android.view.accessibility.AccessibilityManager + + if (accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN) + .isNotEmpty() + ) { + mainScreenController.toolTippsShown() } } var profileToRename by remember { @@ -164,7 +154,16 @@ internal fun MainScreenScaffoldContainer( val toolTipBounds = remember { mutableStateOf>(emptyMap()) } - ToolTips(settingsController, isInPrescriptionScreen, toolTipBounds) + if (startToolTips) { + ToolTips( + isInPrescriptionScreen, + toolTipBounds + ) { + startToolTips = false + mainScreenController.toolTippsShown() + } + } + val coroutineScope = rememberCoroutineScope() BackHandler(enabled = sheetState.isVisible) { coroutineScope.launch { @@ -180,6 +179,7 @@ internal fun MainScreenScaffoldContainer( sheetContent = { MainScreenBottomSheetContentState( mainNavController = mainNavController, + mainScreenController = mainScreenController, profilesController = profilesController, infoContentState = mainScreenBottomSheetContentState, profileToRename = profileToRename, @@ -195,10 +195,10 @@ internal fun MainScreenScaffoldContainer( ExternalAuthenticationDialog() MainScreenScaffold( mainScreenController = mainScreenController, - settingsController = settingsController, profilesController = profilesController, mainNavController = mainNavController, bottomNavController = bottomNavController, + showToolTipps = startToolTips, tooltipBounds = toolTipBounds, onClickAddProfile = { mainScreenBottomSheetContentState = @@ -211,52 +211,8 @@ internal fun MainScreenScaffoldContainer( onClickAvatar = { mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfilePicture() }, + onClickAddPrescription = onClickAddPrescription, scaffoldState = scaffoldState ) } } - -@Composable -private fun CheckInsecureDevice(onDeviceIsInsecure: () -> Unit) { - val settingsController = rememberSettingsController() - LaunchedEffect(Unit) { - if (BuildConfig.DEBUG) { - return@LaunchedEffect - } - @Requirement( - "O.Plat_1#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Navigate to insecure Devices warning." - ) - ( - withContext(Dispatchers.Main) { - if (settingsController.showInsecureDevicePrompt.first()) { - onDeviceIsInsecure() - } - } - ) - } -} - -@Composable -private fun CheckDeviceIntegrity( - mainScreenController: MainScreenController, - mainNavController: NavController -) { - LaunchedEffect(Unit) { - if (BuildConfig.DEBUG) { - return@LaunchedEffect - } - if (mainScreenController.checkDeviceIntegrity().first()) { - withContext(Dispatchers.Main) { - mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) - navOptions { - launchSingleTop = true - popUpTo(MainNavigationScreens.IntegrityNotOkScreen.path()) { - inclusive = true - } - } - } - } - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt index adc1a90c..2dd2d672 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt @@ -35,9 +35,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,35 +48,30 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.profiles.presentation.ProfilesController import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.TopAppBarWithContent import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch /** * The top appbar of the actual main screen. */ @Composable internal fun MultiProfileTopAppBar( - navController: NavController, mainScreenController: MainScreenController, - settingsController: SettingsController, profilesController: ProfilesController, isInPrescriptionScreen: Boolean, elevated: Boolean, onClickAddProfile: () -> Unit, onClickChangeProfileName: (profile: Profile) -> Unit, + onClickAddPrescription: () -> Unit, + showToolTipps: Boolean, tooltipBounds: MutableState> ) { val profiles by profilesController.getProfilesState() @@ -86,14 +79,6 @@ internal fun MultiProfileTopAppBar( val accScan = stringResource(R.string.main_scan_acc) val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else 0.dp } - val toolTipBoundsRequired by produceState(initialValue = false) { - settingsController.showMainScreenToolTips().collect { - value = it - } - } - - val scope = rememberCoroutineScope() - TopAppBarWithContent( title = { MainScreenTopBarTitle(isInPrescriptionScreen) @@ -104,20 +89,12 @@ internal fun MultiProfileTopAppBar( if (isInPrescriptionScreen) { // data matrix code scanner IconButton( - onClick = { - scope.launch { - if (settingsController.mlKitNotAccepted().first()) { - navController.navigate(MainNavigationScreens.MlKitIntroScreen.path()) - } else { - navController.navigate(MainNavigationScreens.Camera.path()) - } - } - }, + onClick = onClickAddPrescription, modifier = Modifier .testTag("erx_btn_scn_prescription") .semantics { contentDescription = accScan } .onGloballyPositioned { coordinates -> - if (toolTipBoundsRequired) { + if (showToolTipps) { tooltipBounds.value += Pair(0, coordinates.boundsInRoot()) } } @@ -137,7 +114,7 @@ internal fun MultiProfileTopAppBar( profiles = profiles, activeProfile = activeProfile, tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired, + toolTipBoundsRequired = showToolTipps, onClickChangeProfileName = onClickChangeProfileName, onClickAddProfile = onClickAddProfile, onClickChangeActiveProfile = { profile -> diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt index 56814fd2..d7026889 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt @@ -38,9 +38,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect @@ -54,11 +52,9 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.Dialog -import kotlinx.coroutines.launch enum class ArrowPosition { Top, @@ -73,20 +69,13 @@ data class ToolTipState( @Composable fun ToolTips( - settingsController: SettingsController, isInPrescriptionScreen: Boolean, - toolTipBounds: MutableState> + toolTipBounds: MutableState>, + onToolTipsShown: () -> Unit ) { - val coroutineScope = rememberCoroutineScope() - var tooltipNr by remember { mutableStateOf(0) } - val showMainScreenTooltips by produceState(initialValue = false) { - settingsController.showMainScreenToolTips().collect { - value = it - } - } - if (isInPrescriptionScreen && showMainScreenTooltips) { + if (isInPrescriptionScreen) { when (tooltipNr) { 0 -> ToolTip( onDismissRequest = { @@ -111,11 +100,7 @@ fun ToolTips( ) 2 -> ToolTip( - onDismissRequest = { - coroutineScope.launch { - settingsController.mainScreenTooltipsShown() - } - }, + onDismissRequest = onToolTipsShown, tooltipState = ToolTipState( arrowPosition = ArrowPosition.Top, elementBound = toolTipBounds.value[2] ?: Rect.Zero @@ -153,7 +138,7 @@ fun ToolTipWithArrowTop( val offset = with(LocalDensity.current) { DpOffset( x = tooltipState.elementBound.bottomCenter.x.toDp() - 12.dp, - y = tooltipState.elementBound.bottomCenter.y.toDp() + 4.dp + y = tooltipState.elementBound.bottomCenter.y.toDp() - 36.dp ) } @@ -199,7 +184,7 @@ fun ToolTipWithArrowRight( val offset = with(LocalDensity.current) { DpOffset( x = tooltipState.elementBound.centerLeft.x.toDp() - 231.dp, - y = tooltipState.elementBound.centerLeft.y.toDp() - 12.dp + y = tooltipState.elementBound.centerLeft.y.toDp() - 50.dp ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt new file mode 100644 index 00000000..43704911 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavGraphBuilderExtension.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import de.gematik.ti.erp.app.navigation.Screen.Companion.track + +fun NavGraphBuilder.renderComposable( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + enterTransition: ( + @JvmSuppressWildcards + AnimatedContentTransitionScope.() -> EnterTransition? + )? = null, + exitTransition: ( + @JvmSuppressWildcards + AnimatedContentTransitionScope.() -> ExitTransition? + )? = null, + popEnterTransition: ( + @JvmSuppressWildcards + AnimatedContentTransitionScope.() -> EnterTransition? + )? = enterTransition, + popExitTransition: ( + @JvmSuppressWildcards + AnimatedContentTransitionScope.() -> ExitTransition? + )? = exitTransition, + screen: AnimatedContentScope.(NavBackStackEntry) -> Screen +) { + composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = enterTransition, + exitTransition = exitTransition, + popEnterTransition = popEnterTransition, + popExitTransition = popExitTransition, + content = { + screen(this, it).track(route).Content() + } + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationAnimationUtils.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationAnimationUtils.kt new file mode 100644 index 00000000..fd3ae4de --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationAnimationUtils.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically + +val enterTransition: EnterTransition by lazy { + slideInVertically( + initialOffsetY = { -300 }, + animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) +} + +val exitTransition: ExitTransition by lazy { + slideOutVertically( + targetOffsetY = { -300 }, + animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) +} + +val popEnterTransition: EnterTransition by lazy { + slideInHorizontally( + initialOffsetX = { -300 }, + animationSpec = tween(300) + ) + fadeIn(animationSpec = tween(300)) +} + +val popExitTransition: ExitTransition by lazy { + slideOutHorizontally( + targetOffsetX = { 300 }, + animationSpec = tween(300) + ) + fadeOut(animationSpec = tween(300)) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt new file mode 100644 index 00000000..141d2191 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRouteNames.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +enum class NavigationRouteNames { + // App-security + DeviceCheckLoadingScreen, + InsecureDeviceScreen, + IntegrityWarningScreen, + + // Edit-profile + ProfileDetailsScreen, + ProfileColorAndImagePickerScreen, + ProfileImageCropperScreen, + ProfileTokenScreen, +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt new file mode 100644 index 00000000..99a2cb9a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationRoutes.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +interface NavigationRoutes { + + fun subGraphName(): String +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt new file mode 100644 index 00000000..330edfd6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/NavigationType.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +/** + * This NavigationType allows us to parse complex objects from one screen to another + */ +@OptIn(ExperimentalContracts::class) +inline fun T.toNavigationString(): String { + contract { + returns() implies (this@toNavigationString is Serializable) + } + return Json.encodeToString(this) +} + +inline fun fromNavigationString(value: String): T { + return Json.decodeFromString(value) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt similarity index 72% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt index 34de90fb..41bd37a4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Routes.kt @@ -16,48 +16,21 @@ * */ -package de.gematik.ti.erp.app +package de.gematik.ti.erp.app.navigation import android.net.Uri -import android.os.Bundle import android.os.Parcelable import androidx.compose.runtime.Immutable import androidx.navigation.NamedNavArgument import androidx.navigation.NavType -import de.gematik.ti.erp.app.mainscreen.navigation.TaskIds -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json abstract class UriNavType(override val isNullableAllowed: Boolean) : NavType(isNullableAllowed) { abstract fun serializeValue(value: T): String } -object AppNavTypes { - val TaskIdsType = object : UriNavType(false) { - override fun put(bundle: Bundle, key: String, value: TaskIds) { - bundle.putParcelable(key, value) - } - - override fun get(bundle: Bundle, key: String): TaskIds { - return bundle.getParcelable(key)!! - } - - override fun parseValue(value: String): TaskIds { - return Json.decodeFromString(value) - } - - override fun serializeValue(value: TaskIds): String { - return Json.encodeToString(value) - } - - override val name: String - get() = "taskIds" - } -} - @Immutable -open class Route(private val path: String, vararg arguments: NamedNavArgument) { +open class Routes(private val path: String, vararg arguments: NamedNavArgument) { val route = arguments.fold(Uri.Builder().path(path)) { uri, param -> uri.appendQueryParameter(param.name, "{${param.name}}") }.build().toString() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt new file mode 100644 index 00000000..1cd2fdb8 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/navigation/Screen.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import com.contentsquare.android.compose.analytics.TriggeredOnResume +import de.gematik.ti.erp.app.analytics.mapper.ContentSquareMapper +import io.github.aakira.napier.Napier + +abstract class Screen { + + abstract val navController: NavController + + abstract val navBackStackEntry: NavBackStackEntry + + val trackerMapper: ContentSquareMapper = ContentSquareMapper() + + @Composable + abstract fun Content() + + @Composable + fun trackScreen(screenName: String?) { + TriggeredOnResume { + // TODO: Track everytime by checking if it exists + Napier.d { "TODO: Tracking logger $screenName" } + // Contentsquare.send(screenName) + } + } + + companion object { + @Composable + fun Screen.track(route: String): Screen { + val routeEnum = when { + route.contains("?") -> route.split(("?"))[0] + else -> route + } + val trackedScreenName = trackerMapper.map(NavigationRouteNames.valueOf(routeEnum)) + trackScreen(trackedScreenName) + return this + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt index 06200df2..4dd1d55c 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt @@ -22,9 +22,7 @@ package de.gematik.ti.erp.app.onboarding.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -38,7 +36,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Switch @@ -52,7 +49,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -63,7 +59,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -74,16 +69,14 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.ui.AllowAnalyticsScreen import de.gematik.ti.erp.app.settings.ui.AllowBiometryScreen -import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.NavigationAnimation @@ -98,18 +91,16 @@ import de.gematik.ti.erp.app.utils.compose.visualTestTag import de.gematik.ti.erp.app.webview.URI_DATA_TERMS import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE import de.gematik.ti.erp.app.webview.WebViewScreen -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlin.math.max import kotlin.math.min object OnboardingNavigationScreens { - object Onboarding : Route("onboarding") - object Analytics : Route("onboarding_analytics") - object TermsOfUse : Route("onboarding_termsOfUse") - object DataProtection : Route("onboarding_dataProtection") - object Biometry : Route("onboarding_biometry") + object Onboarding : Routes("onboarding") + object Analytics : Routes("onboarding_analytics") + object TermsOfUse : Routes("onboarding_termsOfUse") + object DataProtection : Routes("onboarding_dataProtection") + object Biometry : Routes("onboarding_biometry") } private enum class OnboardingPages(val index: Int) { @@ -130,13 +121,22 @@ private enum class OnboardingPages(val index: Int) { @Composable fun OnboardingScreen( - mainNavController: NavController, - settingsController: SettingsController + onOnboardingSucceeded: () -> Unit ) { val navController = rememberNavController() - val coroutineScope = rememberCoroutineScope() + val onboardingController = rememberOnboardingController() - var allowAnalytics by rememberSaveable { mutableStateOf(false) } + val isAnalyticsAllowed by onboardingController.isAnalyticsAllowedState + + LaunchedEffect(Unit) { + // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots + // (on debug builds should be allowed for testing) + if (BuildConfig.DEBUG) { + onboardingController.allowScreenshots(true) + } + } + + var allowAnalytics by rememberSaveable { mutableStateOf(isAnalyticsAllowed) } var secureMethod by rememberSaveable { mutableStateOf(OnboardingSecureAppMethod.None) } val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) @@ -154,34 +154,27 @@ fun OnboardingScreen( secureMethod = it }, allowTracking = allowAnalytics, - onAllowTracking = { + onAllowAnalytics = { + onboardingController.changeAnalyticsState(it) allowAnalytics = it }, onSaveNewUser = { allowTracking, defaultProfileName, secureMethod -> - coroutineScope.launch(Dispatchers.Main) { - settingsController.onboardingSucceeded( - authenticationMode = when (secureMethod) { - is OnboardingSecureAppMethod.DeviceSecurity -> - SettingsData.AuthenticationMode.DeviceSecurity - - is OnboardingSecureAppMethod.Password -> - SettingsData.AuthenticationMode.Password( - password = requireNotNull(secureMethod.checkedPassword) - ) - - else -> error("Illegal state. Authentication must be set") - }, - defaultProfileName = defaultProfileName, - allowTracking = allowTracking - ) - - mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) { - launchSingleTop = true - popUpTo(MainNavigationScreens.Onboarding.path()) { - inclusive = true - } - } - } + onboardingController.onboardingSucceeded( + authenticationMode = when (secureMethod) { + is OnboardingSecureAppMethod.DeviceSecurity -> + SettingsData.AuthenticationMode.DeviceSecurity + + is OnboardingSecureAppMethod.Password -> + SettingsData.AuthenticationMode.Password( + password = requireNotNull(secureMethod.checkedPassword) + ) + + else -> error("Illegal state. Authentication must be set") + }, + profileName = defaultProfileName, + allowAnalytics = allowTracking + ) + onOnboardingSucceeded() } ) } @@ -246,7 +239,7 @@ private fun OnboardingScreenWithScaffold( secureMethod: OnboardingSecureAppMethod, onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit, allowTracking: Boolean, - onAllowTracking: (Boolean) -> Unit, + onAllowAnalytics: (Boolean) -> Unit, onSaveNewUser: ( allowTracking: Boolean, defaultProfileName: String, @@ -275,7 +268,7 @@ private fun OnboardingScreenWithScaffold( secureMethod, onSaveNewUser, allowTracking, - onAllowTracking, + onAllowAnalytics, onSecureMethodChange ) { page = it @@ -293,7 +286,6 @@ private fun OnboardingScreenWithScaffold( } } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun OnboardingPages( page: OnboardingPages, @@ -342,7 +334,9 @@ private fun OnboardingPages( secureMethod = secureMethod, onSecureMethodChange = onSecureMethodChange, onOpenBiometricScreen = { - navController.navigate(OnboardingNavigationScreens.Biometry.path()) + navController.navigate(OnboardingNavigationScreens.Biometry.path()) { + launchSingleTop = true + } }, onNextPage = { onNextPage(OnboardingPages.Analytics) } ) @@ -677,7 +671,8 @@ private fun AnalyticsToggle( "A_19980#2", "A_19981#2", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "The user is informed and required to accept this information via the data protection statement. Related data and services are listed in sections 5." // ktlint-disable max-line-length + rationale = "The user is informed and required to accept this information via the data protection statement." + + " Related data and services are listed in sections 5." ) @Composable private fun DataTermsToggle( @@ -703,21 +698,15 @@ private fun LargeToggle( .clip(RoundedCornerShape(PaddingDefaults.Medium)) .background(AppTheme.colors.neutral100, shape = RoundedCornerShape(16.dp)) .fillMaxWidth() - .toggleable( - value = checked, - onValueChange = onCheckedChange, - enabled = true, - role = Role.Switch, - interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current - ) .padding(PaddingDefaults.Medium), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) ) { Switch( checked = checked, - onCheckedChange = null + onCheckedChange = onCheckedChange, + enabled = true, + interactionSource = remember { MutableInteractionSource() } ) SpacerSmall() Text( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingController.kt new file mode 100644 index 00000000..cbf2756b --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingController.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase +import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingSuccededUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class OnboardingController( + private val saveOnboardingSuccededUseCase: SaveOnboardingSuccededUseCase, + private val allowScreenshotsUseCase: AllowScreenshotsUseCase, + private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, + private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + private val analytics: Analytics, + private val scope: CoroutineScope +) { + + private val isAnalyticsAllowed by lazy { + isAnalyticsAllowedUseCase.invoke() + } + + val isAnalyticsAllowedState + @Composable + get() = isAnalyticsAllowed.collectAsStateWithLifecycle(false) + + fun onboardingSucceeded( + authenticationMode: SettingsData.AuthenticationMode, + profileName: String, + allowAnalytics: Boolean + ) = scope.launch { + analytics.setAnalyticsPreference(allowAnalytics) + saveOnboardingSuccededUseCase( + authenticationMode = authenticationMode, + profileName = profileName + ) + } + + fun allowScreenshots(allow: Boolean) { + scope.launch { + allowScreenshotsUseCase(allow) + } + } + + fun changeAnalyticsState(state: Boolean) { + scope.launch { + changeAnalyticsStateUseCase.invoke(state) + } + } +} + +@Composable +fun rememberOnboardingController(): OnboardingController { + val saveOnboardingSuccededUseCase by rememberInstance() + val allowScreenshotsUseCase by rememberInstance() + val isAnalyticsAllowedUseCase by rememberInstance() + val changeAnalyticsStateUseCase by rememberInstance() + val scope = rememberCoroutineScope() + val analytics by rememberInstance() + + return remember { + OnboardingController( + saveOnboardingSuccededUseCase = saveOnboardingSuccededUseCase, + allowScreenshotsUseCase = allowScreenshotsUseCase, + isAnalyticsAllowedUseCase = isAnalyticsAllowedUseCase, + changeAnalyticsStateUseCase = changeAnalyticsStateUseCase, + analytics = analytics, + scope = scope + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt index 4605bbe4..d4f231f3 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt @@ -81,7 +81,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData import de.gematik.ti.erp.app.settings.ui.openMailClient @@ -104,7 +104,7 @@ fun HealthCardContactOrderScreen( val navController = rememberNavController() var previousNavEntry by remember { mutableStateOf("contactInsuranceCompany") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val title = stringResource(R.string.health_insurance_search_page_title) val navigationMode by navController.navigationModeState(HealthCardOrderNavigationScreens.HealthCardOrder.route) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt index 204e9e5f..e06a352e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.orderhealthcard.usecase.HealthCardOrderUseCase import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData import kotlinx.coroutines.flow.MutableStateFlow @@ -30,9 +30,9 @@ import kotlinx.coroutines.flow.combine import org.kodein.di.compose.rememberInstance object HealthCardOrderNavigationScreens { - object HealthCardOrder : Route("contactInsuranceCompany") - object SelectOrderOption : Route("contactInsuranceCompany_selectReason") - object HealthCardOrderContact : Route("contactInsuranceCompany_selectMethod") + object HealthCardOrder : Routes("contactInsuranceCompany") + object SelectOrderOption : Routes("contactInsuranceCompany_selectReason") + object HealthCardOrderContact : Routes("contactInsuranceCompany_selectMethod") } class HealthCardOrderState( diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt index f2433e1d..5d5536d2 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.setValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController @@ -126,7 +126,7 @@ fun PharmacyNavigation( } var previousNavEntry by remember { mutableStateOf("pharmacySearch") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val handleSearchResultFn = { searchResult: PharmacySearchController.SearchQueryResult -> when (searchResult) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt index db5d99f7..aa3bd2d7 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt @@ -47,14 +47,12 @@ 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.unit.dp import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.deliveryUrlNotEmpty import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isDeliveryWithoutContactUrls import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isOnlineServiceWithoutContactUrls -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isPickupWithoutContactUrls import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.onlineUrlNotEmpty import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.pickupUrlNotEmpty import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData @@ -65,6 +63,8 @@ import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.shortToast private const val MAX_OPTIONS = 3 +private const val DISABLED_ALPHA = 0.3f +private const val ENABLED_ALPHA = 1f @Composable internal fun OrderSelection( @@ -74,43 +74,25 @@ internal fun OrderSelection( ) { val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState - val hasNoPickupContact = pharmacy.contacts.pickUpUrl.isEmpty() - val hasNoDeliveryContact = pharmacy.contacts.pickUpUrl.isEmpty() - val hasNoOnlineServiceContact = pharmacy.contacts.pickUpUrl.isEmpty() - val hasNoContacts = listOf(hasNoPickupContact, hasNoDeliveryContact, hasNoOnlineServiceContact) + val directRedeemUrlsNotPresent = pharmacy.directRedeemUrlsNotPresent - val hasNoContactUrls = hasNoContacts.all { it } + val (pickUpContactAvailable, deliveryContactAvailable, onlineContactAvailable) = + pharmacy.checkRedemptionAndContactAvailabilityForPharmacy(directRedeemEnabled) - // service availability checks - val pickUpServiceAvailable = directRedeemEnabled && pharmacy.pickupUrlNotEmpty() - val deliveryServiceAvailable = directRedeemEnabled && pharmacy.deliveryUrlNotEmpty() - val onlineServiceAvailable = directRedeemEnabled && pharmacy.onlineUrlNotEmpty() + val (pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) = + pharmacy.checkServiceVisibility( + directRedeemUrlsNotPresent = directRedeemUrlsNotPresent, + deliveryServiceAvailable = deliveryContactAvailable, + onlineServiceAvailable = onlineContactAvailable + ) - // visibility checks - val pickUpServiceVisible = pickUpServiceAvailable || - pharmacy.pickupUrlNotEmpty() || - pharmacy.isPickupWithoutContactUrls(hasNoContactUrls) - - val deliveryServiceVisible = deliveryServiceAvailable || - pharmacy.deliveryUrlNotEmpty() || - pharmacy.isDeliveryWithoutContactUrls(hasNoContactUrls) - - val onlineServiceVisible = onlineServiceAvailable || - pharmacy.onlineUrlNotEmpty() || - pharmacy.isOnlineServiceWithoutContactUrls(hasNoContactUrls) - - // enabled checks - val pickupServiceEnabled = pharmacy.pickupUrlNotEmpty() || - pickUpServiceAvailable || - !directRedeemEnabled && pharmacy.isPickupService - - val deliveryServiceEnabled = pharmacy.deliveryUrlNotEmpty() || - deliveryServiceAvailable || - !directRedeemEnabled && pharmacy.isDeliveryService - - val onlineServiceEnabled = pharmacy.onlineUrlNotEmpty() || - onlineServiceAvailable || - !directRedeemEnabled && pharmacy.isOnlineService + val (pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) = + pharmacy.checkServiceAvailability( + directRedeemEnabled = directRedeemEnabled, + pickUpContactAvailable = pickUpContactAvailable, + deliveryContactAvailable = deliveryContactAvailable, + onlineContactAvailable = onlineContactAvailable + ) val numberOfServices = remember(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) { listOf(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible).count { it } @@ -170,7 +152,6 @@ internal fun OrderSelection( } } -@Suppress("MagicNumber") @Composable private fun OrderButton( modifier: Modifier, @@ -180,7 +161,7 @@ private fun OrderButton( image: Painter, onClick: () -> Unit ) { - val shape = RoundedCornerShape(16.dp) + val shape = RoundedCornerShape(PaddingDefaults.Medium) val serviceDisabledText = stringResource(R.string.connect_for_pharmacy_service) var showToast by remember { mutableStateOf(false) } @@ -205,8 +186,8 @@ private fun OrderButton( .padding(PaddingDefaults.Medium) .alpha( when { - isServiceEnabled -> 1f - else -> 0.3f + isServiceEnabled -> ENABLED_ALPHA + else -> DISABLED_ALPHA } ) ) { @@ -228,3 +209,54 @@ private fun OrderButton( shortToast(serviceDisabledText) } } + +private fun Pharmacy.checkRedemptionAndContactAvailabilityForPharmacy( + directRedeemEnabled: Boolean +): Triple { + val pickUpServiceAvailable = directRedeemEnabled && pickupUrlNotEmpty() + + val deliveryServiceAvailable = directRedeemEnabled && deliveryUrlNotEmpty() + + val onlineServiceAvailable = directRedeemEnabled && onlineUrlNotEmpty() + + return Triple(pickUpServiceAvailable, deliveryServiceAvailable, onlineServiceAvailable) +} + +private fun Pharmacy.checkServiceVisibility( + directRedeemUrlsNotPresent: Boolean, + deliveryServiceAvailable: Boolean, + onlineServiceAvailable: Boolean +): Triple { + val pickUpServiceVisible = pickupUrlNotEmpty() || directRedeemUrlsNotPresent + + val deliveryServiceVisible = deliveryServiceAvailable || + deliveryUrlNotEmpty() || + isDeliveryWithoutContactUrls(directRedeemUrlsNotPresent) + + val onlineServiceVisible = onlineServiceAvailable || + onlineUrlNotEmpty() || + isOnlineServiceWithoutContactUrls(directRedeemUrlsNotPresent) + + return Triple(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) +} + +private fun Pharmacy.checkServiceAvailability( + directRedeemEnabled: Boolean, + pickUpContactAvailable: Boolean, + deliveryContactAvailable: Boolean, + onlineContactAvailable: Boolean +): Triple { + val pickupServiceEnabled = pickupUrlNotEmpty() || + pickUpContactAvailable || + !directRedeemEnabled && isPickupService + + val deliveryServiceEnabled = deliveryUrlNotEmpty() || + deliveryContactAvailable || + !directRedeemEnabled && isDeliveryService + + val onlineServiceEnabled = onlineUrlNotEmpty() || + onlineContactAvailable || + !directRedeemEnabled && isOnlineService + + return Triple(pickupServiceEnabled, deliveryServiceEnabled, onlineServiceEnabled) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt index a2bf3c27..39a578a8 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt @@ -18,16 +18,16 @@ package de.gematik.ti.erp.app.pharmacy.ui.model -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.prescription.detail.ui.model.PopUpName object PharmacyNavigationScreens { - object StartSearch : Route("pharmacySearch") - object List : Route("pharmacySearch_detail") - object Maps : Route("pharmacySearch_map") - object OrderOverview : Route("redeem_viaTI") // TODO change when redeem_viaAVS is available - object EditShippingContact : Route("redeem_editContactInformation") - object PrescriptionSelection : Route("redeem_prescriptionChooseSubset") + object StartSearch : Routes("pharmacySearch") + object List : Routes("pharmacySearch_detail") + object Maps : Routes("pharmacySearch_map") + object OrderOverview : Routes("redeem_viaTI") // TODO change when redeem_viaAVS is available + object EditShippingContact : Routes("redeem_editContactInformation") + object PrescriptionSelection : Routes("redeem_prescriptionChooseSubset") } object PharmacySearchPopUpNames { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt index 1a0cbdb9..3d128e54 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt @@ -43,7 +43,7 @@ val prescriptionModule = DI.Module("prescriptionModule") { bindSingleton { PrescriptionLocalDataSource(instance()) } bindSingleton { PrescriptionRemoteDataSource(instance()) } bindSingleton { PrescriptionUseCase(instance(), instance(), instance()) } - bindSingleton { RefreshPrescriptionUseCase(instance(), instance(), instance()) } + bindSingleton { RefreshPrescriptionUseCase(instance(), instance(), instance(), instance(), instance()) } bindProvider { GetActivePrescriptionsUseCase(instance()) } bindProvider { GetArchivedPrescriptionsUseCase(instance()) } bindProvider { UpdateScannedTaskNameUseCase(instance()) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt index e00829f4..b3aaeabb 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt @@ -63,7 +63,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.analytics.trackPrescriptionDetailPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics @@ -103,7 +103,7 @@ fun PrescriptionDetailsScreen( val navController = rememberNavController() var previousNavEntry by remember { mutableStateOf("prescriptionDetail") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController = navController, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt index cfb1637e..aee68dec 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt @@ -33,7 +33,7 @@ import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationModeAndMethod +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod import de.gematik.ti.erp.app.utils.compose.shortToast import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt index 904a48f4..44754589 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.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 @@ -45,8 +44,7 @@ fun TechnicalInformation( prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { - val prescription by prescriptionDetailsController.prescriptionState - val syncedPrescription = prescription as? PrescriptionData.Synced + val prescriptionState by prescriptionDetailsController.prescriptionState val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -66,7 +64,7 @@ fun TechnicalInformation( item { SpacerMedium() } - syncedPrescription?.accessCode?.let { + prescriptionState?.accessCode?.let { item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode), @@ -79,7 +77,7 @@ fun TechnicalInformation( item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.TaskId), - text = syncedPrescription?.taskId, + text = taskId, label = stringResource(R.string.task_id) ) SpacerMedium() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt index 6e52f7b3..3301fae4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt @@ -19,18 +19,18 @@ package de.gematik.ti.erp.app.prescription.detail.ui.model import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes object PrescriptionDetailsNavigationScreens { - object Overview : Route("prescriptionDetail") - object MedicationOverview : Route("prescriptionDetail_medicationOverview") - object Medication : Route("prescriptionDetail_medication") - object Patient : Route("prescriptionDetail_patient") - object Prescriber : Route("prescriptionDetail_practitioner") - object Organization : Route("prescriptionDetail_organization") - object Accident : Route("prescriptionDetail_accidentInfo") - object TechnicalInformation : Route("prescriptionDetail_technicalInfo") - object Ingredient : Route("prescriptionDetail_medication_ingredients") + object Overview : Routes("prescriptionDetail") + object MedicationOverview : Routes("prescriptionDetail_medicationOverview") + object Medication : Routes("prescriptionDetail_medication") + object Patient : Routes("prescriptionDetail_patient") + object Prescriber : Routes("prescriptionDetail_practitioner") + object Organization : Routes("prescriptionDetail_organization") + object Accident : Routes("prescriptionDetail_accidentInfo") + object TechnicalInformation : Routes("prescriptionDetail_technicalInfo") + object Ingredient : Routes("prescriptionDetail_medication_ingredients") } object PrescriptionDetailsPopUpNames { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt index 412144d1..c7c5dc04 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt @@ -81,7 +81,7 @@ fun SmallMainScreenAvatar( modifier = Modifier.size(16.dp), emptyIcon = Icons.Rounded.AddAPhoto, profile = profile, - figure = profile.avatar + avatar = profile.avatar ) } } @@ -157,7 +157,7 @@ fun MainScreenAvatar( modifier = Modifier.size(24.dp), emptyIcon = Icons.Rounded.AddAPhoto, profile = profile, - figure = profile.avatar + avatar = profile.avatar ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt index 8fc09478..03f71107 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt @@ -32,18 +32,14 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R -import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens -import de.gematik.ti.erp.app.settings.ui.SettingsController 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 @@ -52,7 +48,6 @@ import de.gematik.ti.erp.app.utils.compose.PrimaryButton import de.gematik.ti.erp.app.utils.compose.SecondaryButton import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import kotlinx.coroutines.launch @Requirement( "A_20194", @@ -61,11 +56,11 @@ import kotlinx.coroutines.launch ) @Composable fun MlKitIntroScreen( - navController: NavController, - settingsController: SettingsController + onClickReadMore: () -> Unit, + onAcceptMLKit: () -> Unit, + onBack: () -> Unit ) { val listState = rememberLazyListState() - val scope = rememberCoroutineScope() AnimatedElevationScaffold( modifier = Modifier @@ -73,22 +68,13 @@ fun MlKitIntroScreen( topBarTitle = "", bottomBar = { MlKitBottomBar( - onAccept = { - scope.launch { - settingsController.acceptMlKit() - } - navController.navigate(MainNavigationScreens.Camera.path()) - }, - onClickReadMore = { - navController.navigate(MainNavigationScreens.MlKitInformationScreen.path()) - } + onAccept = onAcceptMLKit, + onClickReadMore = onClickReadMore ) }, listState = listState, navigationMode = NavigationBarMode.Close, - onBack = { - navController.popBackStack() - } + onBack = onBack ) { LazyColumn( modifier = Modifier diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt index 71705f42..b9790417 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt @@ -33,6 +33,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn +/** + * The prescription [repository] obtains the active + * scanned and synced prescriptions and sorts them + * by the authored or scanned date in a descending order and then + * by name. + * + */ class GetActivePrescriptionsUseCase( private val repository: PrescriptionRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO @@ -54,7 +61,11 @@ class GetActivePrescriptionsUseCase( .groupByHospitalsOrDoctors() .flatMapToPrescriptions() - (scannedPrescriptions + syncedPrescriptions) - .sortedByDescending { it.redeemedOn ?: it.startedOn } + (scannedPrescriptions + syncedPrescriptions).sortActives() }.flowOn(dispatcher) + + companion object { + private fun List.sortActives() = + sortedWith(compareByDescending { it.startedOn }.thenBy { it.name }) + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt index beb8002e..3206362f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt @@ -31,6 +31,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn +/** + * The prescription [repository] obtains the non-active + * scanned and synced prescriptions and sorts them + * by the redeemed date or expired date in a descending order and then + * by name. + * + */ class GetArchivedPrescriptionsUseCase( private val repository: PrescriptionRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO @@ -50,8 +57,11 @@ class GetArchivedPrescriptionsUseCase( .filterNonActiveTasks() .map(SyncedTask::toPrescription) - (syncedPrescriptions + scannedPrescriptions) - .sortedBy { it.taskId } - .sortedByDescending { it.redeemedOn ?: it.startedOn } + (syncedPrescriptions + scannedPrescriptions).sortArchives() }.flowOn(dispatcher) + + companion object { + private fun List.sortArchives() = + sortedWith(compareByDescending { it.redeemedOn ?: it.expiresOn }.thenBy { it.name }) + } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt index a863b092..0a923498 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt @@ -19,9 +19,11 @@ package de.gematik.ti.erp.app.prescription.usecase import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository import de.gematik.ti.erp.app.orders.repository.CommunicationRepository import de.gematik.ti.erp.app.prescription.repository.TaskRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import io.github.aakira.napier.Napier import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -37,6 +39,8 @@ import kotlinx.coroutines.withContext class RefreshPrescriptionUseCase( private val repository: TaskRepository, private val communicationRepository: CommunicationRepository, + private val invoiceRepository: InvoiceRepository, + private val profilesRepository: ProfileRepository, dispatchers: DispatchProvider ) { private class Request( @@ -64,6 +68,9 @@ class RefreshPrescriptionUseCase( val result = runCatching { val nrOfNewPrescriptions = repository.downloadTasks(profileId).getOrThrow() communicationRepository.downloadCommunications(profileId).getOrThrow() + if (profilesRepository.checkIsProfilePKV(profileId)) { + invoiceRepository.downloadInvoices(profileId) + } nrOfNewPrescriptions } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt index c443b79a..3864f0b4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt @@ -25,9 +25,11 @@ import kotlinx.datetime.Instant @Immutable sealed interface Prescription { + val name: String? val taskId: String val redeemedOn: Instant? val startedOn: Instant? + val expiresOn: Instant? /** * Represents a single [Task] synchronized with the backend. @@ -35,13 +37,13 @@ sealed interface Prescription { @Immutable data class SyncedPrescription( override val taskId: String, + override val name: String?, override val redeemedOn: Instant?, + override val expiresOn: Instant?, val state: SyncedTaskData.SyncedTask.TaskState, - val name: String?, val isIncomplete: Boolean, val organization: String, val authoredOn: Instant, - val expiresOn: Instant?, val acceptUntil: Instant?, val isDirectAssignment: Boolean, val prescriptionChipInformation: PrescriptionChipInformation @@ -62,13 +64,14 @@ sealed interface Prescription { @Immutable data class ScannedPrescription( override val taskId: String, + override val name: String?, override val redeemedOn: Instant?, val scannedOn: Instant, - val name: String?, val index: Int, val communications: List ) : Prescription { override val startedOn = scannedOn + override val expiresOn = null } fun redeemedOrExpiredOn(): Instant = diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt index 4ee544a3..0dff0b2f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt @@ -77,7 +77,7 @@ fun Avatar( profile = profile, emptyIcon = emptyIcon, modifier = iconModifier, - figure = profile.avatar + avatar = profile.avatar ) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt index f0350207..1097d7dd 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt @@ -32,9 +32,9 @@ import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument -import de.gematik.ti.erp.app.Route -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.pkv.ui.InvoiceDetailsScreen import de.gematik.ti.erp.app.pkv.ui.InvoiceInformationScreen import de.gematik.ti.erp.app.pkv.ui.InvoiceLocalCorrectionScreen @@ -50,16 +50,16 @@ import de.gematik.ti.erp.app.utils.compose.NavigationMode import kotlinx.coroutines.launch object ProfileDestinations { - object Profile : Route("profile") - object Token : Route("profile_token") - object AuditEvents : Route("profile_auditEvents") - object PairedDevices : Route("profile_registeredDevices") - object ProfileImagePicker : Route("profile_editPicture") - object ProfileImageCropper : Route("profile_editPicture_imageCropper") - object Invoices : Route("chargeItem_list") + object Profile : Routes("profile") + object Token : Routes("profile_token") + object AuditEvents : Routes("profile_auditEvents") + object PairedDevices : Routes("profile_registeredDevices") + object ProfileImagePicker : Routes("profile_editPicture") + object ProfileImageCropper : Routes("profile_editPicture_imageCropper") + object Invoices : Routes("chargeItem_list") object InvoiceInformation : - Route( + Routes( "chargeItem_details", navArgument("taskId") { type = NavType.StringType } ) { @@ -67,7 +67,7 @@ object ProfileDestinations { } object InvoiceDetails : - Route( + Routes( "chargeItem_details_expanded", navArgument("taskId") { type = NavType.StringType } ) { @@ -75,7 +75,7 @@ object ProfileDestinations { } object InvoiceLocalCorrection : - Route( + Routes( "chargeItem_correct_locally", navArgument("taskId") { type = NavType.StringType } ) { @@ -83,7 +83,7 @@ object ProfileDestinations { } object ShareInformation : - Route( + Routes( "chargeItem_share", navArgument("taskId") { type = NavType.StringType } ) { @@ -102,7 +102,7 @@ fun EditProfileNavGraph( mainNavController: NavController ) { var previousNavEntry by remember { mutableStateOf("profile") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val scope = rememberCoroutineScope() val invoicesController = rememberInvoicesController(profileId = selectedProfile.id) val auditEventsController = rememberAuditEventsController() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt index cf2d758f..80f2ea8f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt @@ -119,6 +119,7 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.DynamicText +import de.gematik.ti.erp.app.utils.compose.ErrorText import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerSmall @@ -287,7 +288,10 @@ fun EditProfileScreenContent( ProfileNameDialog( profilesController = profilesController, wantRemoveLastProfile = true, - onEdit = { showAddDefaultProfileDialog = false; onRemoveProfile(it) }, + onEdit = { + showAddDefaultProfileDialog = false + onRemoveProfile(it) + }, onDismissRequest = { showAddDefaultProfileDialog = false } ) } @@ -534,7 +538,7 @@ fun ProfileNameSection( append(" ") appendInlineContent("edit", "edit") } - val c = mapOf( + val inlineContent = mapOf( "edit" to InlineTextContent( Placeholder( width = 0.em, @@ -548,7 +552,7 @@ fun ProfileNameSection( DynamicText( txt, style = AppTheme.typography.h5, - inlineContent = c, + inlineContent = inlineContent, modifier = Modifier .clickable { textFieldEnabled = true @@ -587,10 +591,8 @@ fun ProfileNameSection( stringResource(R.string.edit_profile_duplicated_profile_name) } - Text( + ErrorText( text = errorText, - color = AppTheme.colors.red600, - style = AppTheme.typography.caption1, modifier = Modifier.padding(start = PaddingDefaults.Medium) ) } @@ -679,7 +681,7 @@ fun ProfileAvatarSection( emptyIcon = Icons.Rounded.AddAPhoto, modifier = Modifier.size(24.dp), profile = profile, - figure = profile.avatar + avatar = profile.avatar ) } } @@ -702,11 +704,11 @@ fun ChooseAvatar( profile: ProfilesUseCaseData.Profile, emptyIcon: ImageVector, showPersonalizedImage: Boolean = true, - figure: ProfilesData.Avatar + avatar: ProfilesData.Avatar ) { - val imageResource = extractImageResource(useSmallImages, figure) + val imageResource = extractImageResource(useSmallImages, avatar) - when (figure) { + when (avatar) { ProfilesData.Avatar.PersonalizedImage -> { if (showPersonalizedImage) { if (profile.image != null) { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt index 7c1bedb2..f1a00ab1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt @@ -95,18 +95,21 @@ fun ProfileColorAndImagePicker( actions = {} ) } - ) { + ) { paddingValues -> LazyColumn( state = listState, modifier = Modifier - .padding(it) + .padding(paddingValues) .fillMaxSize(), contentPadding = PaddingValues(PaddingDefaults.Medium) ) { item { SpacerMedium() ProfileImage(editableProfile) { - editableProfile = editableProfile.copy(image = null) + editableProfile = editableProfile.copy( + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null + ) clearPersonalizedImage() } } @@ -148,7 +151,7 @@ fun ProfileColorAndImagePicker( @Composable fun AvatarPicker( profile: ProfilesUseCaseData.Profile, - currentAvatar: ProfilesData.Avatar, + currentAvatar: ProfilesData.Avatar?, onPickPersonalizedImage: () -> Unit, onSelectAvatar: (ProfilesData.Avatar) -> Unit ) { @@ -220,7 +223,7 @@ fun AvatarSelector( emptyIcon = Icons.Rounded.AddAPhoto, modifier = Modifier.size(24.dp), profile = profile, - figure = figure + avatar = figure ) } } @@ -276,7 +279,10 @@ fun ColorPicker( } @Composable -fun ProfileImage(selectedProfile: ProfilesUseCaseData.Profile, onClickDeleteAvatar: () -> Unit) { +fun ProfileImage( + selectedProfile: ProfilesUseCaseData.Profile, + onClickDeleteAvatar: () -> Unit +) { val colors = profileColor(profileColorNames = selectedProfile.color) Column( @@ -300,7 +306,7 @@ fun ProfileImage(selectedProfile: ProfilesUseCaseData.Profile, onClickDeleteAvat modifier = Modifier.size(36.dp), profile = selectedProfile, emptyIcon = Icons.Rounded.PersonOutline, - figure = selectedProfile.avatar, + avatar = selectedProfile.avatar, showPersonalizedImage = selectedProfile.image != null ) } @@ -316,7 +322,11 @@ fun ProfileImage(selectedProfile: ProfilesUseCaseData.Profile, onClickDeleteAvat .background(AppTheme.colors.neutral050) .border(1.dp, color = AppTheme.colors.neutral000, shape = RoundedCornerShape(16.dp)) ) { - IconButton(onClick = onClickDeleteAvatar) { + IconButton( + onClick = { + onClickDeleteAvatar() + } + ) { Icon( imageVector = Icons.Rounded.Close, tint = AppTheme.colors.neutral600, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt deleted file mode 100644 index 27cf28fd..00000000 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile - -@Deprecated("Not to be used, left here for reference in case of errors found") -class ProfileHandler { - - enum class ProfileConnectionState { - LoggedIn, - LoggedOutWithoutTokenBiometrics, - LoggedOutWithoutToken, - LoggedOut, - NeverConnected - } - - private fun Profile.neverConnected() = ssoTokenScope == null && lastAuthenticated == null - - private fun Profile.ssoTokenSetAndConnected() = - ssoTokenScope?.token != null && ssoTokenScope?.token?.isValid() == true - - private fun Profile.ssoTokenSetAndDisconnected() = - ssoTokenScope != null && ssoTokenScope?.token?.isValid() == false || - lastAuthenticated != null - - private fun Profile.ssoTokenNotSet() = - when (ssoTokenScope) { - is IdpData.ExternalAuthenticationToken, - is IdpData.AlternateAuthenticationToken, - is IdpData.AlternateAuthenticationWithoutToken, - is IdpData.DefaultToken -> ssoTokenScope?.token == null - - null -> true - } - - private fun Profile.ssoTokenWithoutScope() = - when (ssoTokenScope) { - is IdpData.AlternateAuthenticationWithoutToken -> true - else -> false - } - - @Stable - fun connectionState(profile: Profile): ProfileConnectionState? = - when { - profile.neverConnected() -> - ProfileConnectionState.NeverConnected - - profile.ssoTokenWithoutScope() -> - ProfileConnectionState.LoggedOutWithoutTokenBiometrics - - profile.ssoTokenNotSet() -> - ProfileConnectionState.LoggedOutWithoutToken - - profile.ssoTokenSetAndConnected() -> - ProfileConnectionState.LoggedIn - - profile.ssoTokenSetAndDisconnected() -> - ProfileConnectionState.LoggedOut - - else -> null - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt index ffd90c21..05869e22 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.setValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.pharmacy.ui.PharmacyNavigation import de.gematik.ti.erp.app.pharmacy.ui.PrescriptionSelection import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyOrderController @@ -45,7 +45,7 @@ fun RedeemNavigation( val navigationMode by navController.navigationModeState(RedeemNavigation.MethodSelection.route) var previousNavEntry by remember { mutableStateOf("redeem_methodSelection") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt index 1c26c9f3..88d8711e 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt @@ -18,12 +18,12 @@ package de.gematik.ti.erp.app.redeem.ui.model -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.navigation.Routes class RedeemNavigation { - object MethodSelection : Route("redeem_methodSelection") - object PrescriptionSelection : Route("redeem_prescriptionChooseSubset") - object LocalRedeem : Route("redeem_matrixCode") - object OnlineRedeem : Route("redeem_prescriptionAllOrSelection") - object PharmacySearch : Route("pharmacySearch") + object MethodSelection : Routes("redeem_methodSelection") + object PrescriptionSelection : Routes("redeem_prescriptionChooseSubset") + object LocalRedeem : Routes("redeem_matrixCode") + object OnlineRedeem : Routes("redeem_prescriptionAllOrSelection") + object PharmacySearch : Routes("pharmacySearch") } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt index 3dc15551..14b9cc17 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt @@ -18,8 +18,22 @@ package de.gematik.ti.erp.app.settings +import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase +import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase import de.gematik.ti.erp.app.settings.repository.CardWallRepository import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import de.gematik.ti.erp.app.settings.usecase.AcceptMLKitUseCase +import de.gematik.ti.erp.app.settings.usecase.AllowAnalyticsUseCase +import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetCanStartToolTipsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetMLKitAcceptedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetOnboardingSucceededUseCase +import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase +import de.gematik.ti.erp.app.settings.usecase.GetShowWelcomeDrawerUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveOnboardingSuccededUseCase +import de.gematik.ti.erp.app.settings.usecase.SavePasswordUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveToolTippsShownUseCase +import de.gematik.ti.erp.app.settings.usecase.SaveWelcomeDrawerShownUseCase import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase import org.kodein.di.DI import org.kodein.di.bindProvider @@ -31,4 +45,19 @@ val settingsModule = DI.Module("settingsModule") { bindProvider { CardWallRepository(prefs = instance(ApplicationPreferencesTag)) } bindProvider { SettingsRepository(instance(), instance()) } bindProvider { SettingsUseCase(instance(), instance()) } + bindProvider { IsAnalyticsAllowedUseCase(instance()) } + bindProvider { ChangeAnalyticsStateUseCase(instance()) } + bindProvider { GetScreenShotsAllowedUseCase(instance()) } + bindProvider { AllowScreenshotsUseCase(instance()) } + bindProvider { GetOnboardingSucceededUseCase(instance()) } + bindProvider { SaveOnboardingSuccededUseCase(instance()) } + bindProvider { AcceptMLKitUseCase(instance()) } + bindProvider { AllowAnalyticsUseCase(instance()) } + bindProvider { GetMLKitAcceptedUseCase(instance()) } + bindProvider { GetCanStartToolTipsUseCase(instance()) } + bindProvider { SaveToolTippsShownUseCase(instance()) } + bindProvider { SavePasswordUseCase(instance()) } + bindProvider { GetShowWelcomeDrawerUseCase(instance()) } + bindProvider { SaveWelcomeDrawerShownUseCase(instance()) } + // bindProvider { GetDeviceSecurityUseCase(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt index a258f70c..ae48b676 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt @@ -40,7 +40,7 @@ fun AccessibilitySettingsScreen(onBack: () -> Unit) { val zoomState by settingsController.zoomState val scope = rememberCoroutineScope() val listState = rememberLazyListState() - val screenShotState by settingsController.screenshotState + val screenShotState by settingsController.screenShotsState AnimatedElevationScaffold( topBarTitle = stringResource(R.string.settings_accessibility_headline), @@ -67,16 +67,9 @@ fun AccessibilitySettingsScreen(onBack: () -> Unit) { } item { AllowScreenShotsSection( - screenShotsAllowed = screenShotState.screenshotsAllowed + screenShotsAllowed = screenShotState ) { allow -> - when (allow) { - true -> scope.launch { - settingsController.onAllowScreenshots() - } - false -> scope.launch { - settingsController.onDisAllowScreenshots() - } - } + settingsController.onAllowScreenshots(allow) } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt index 77d9e5b1..a9b0bb07 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.settings.ui +import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding @@ -28,19 +29,17 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics +import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.userauthentication.ui.BiometricPrompt +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode @@ -50,9 +49,28 @@ fun AllowBiometryScreen( onNext: () -> Unit, onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit ) { - var showBiometricPrompt by remember { mutableStateOf(false) } val lazyListState = rememberLazyListState() + val activity = LocalActivity.current + + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } + + val infoBuilder = biometricPromptBuilder.buildPromptInfo( + title = stringResource(R.string.auth_prompt_headline), + negativeButton = stringResource(R.string.auth_prompt_cancel) + ) + + val prompt = remember(biometricPromptBuilder) { + biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + onSecureMethodChange(OnboardingSecureAppMethod.DeviceSecurity) + onNext() + }, + onFailure = { onBack() }, + onError = { onBack() } + ) + } + AnimatedElevationScaffold( modifier = Modifier.navigationBarsPadding(), navigationMode = NavigationBarMode.Close, @@ -60,7 +78,7 @@ fun AllowBiometryScreen( OnboardingBottomBar( buttonText = stringResource(R.string.settings_device_security_allow), onButtonClick = { - showBiometricPrompt = true + prompt.authenticate(infoBuilder) }, buttonEnabled = true, info = null, @@ -71,26 +89,6 @@ fun AllowBiometryScreen( listState = lazyListState, onBack = onBack ) { - if (showBiometricPrompt) { - BiometricPrompt( - title = stringResource(R.string.auth_prompt_headline), - description = "", - negativeButton = stringResource(R.string.auth_prompt_cancel), - onAuthenticated = { - onSecureMethodChange(OnboardingSecureAppMethod.DeviceSecurity) - onNext() - }, - onCancel = { - onBack() - }, - onAuthenticationError = { - onBack() - }, - onAuthenticationSoftError = { - } - ) - } - LazyColumn( state = lazyListState, modifier = Modifier diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt index 3d28aff9..b8c149f1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt @@ -52,7 +52,7 @@ import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.userauthentication.ui.BiometricPrompt +import de.gematik.ti.erp.app.userauthentication.ui.BiometricPromptWrapper import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.NavigationBarMode @@ -70,7 +70,7 @@ fun DeviceSecuritySettingsScreen( var showBiometricPrompt by rememberSaveable { mutableStateOf(false) } if (showBiometricPrompt) { - BiometricPrompt( + BiometricPromptWrapper( title = stringResource(R.string.auth_prompt_headline), description = "", negativeButton = stringResource(R.string.auth_prompt_cancel), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt index 9ff4019c..027aaa7a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt @@ -54,7 +54,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -76,7 +75,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.nulabinc.zxcvbn.Zxcvbn import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.features.R @@ -88,24 +86,22 @@ 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.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import kotlinx.coroutines.launch import java.util.Locale private const val MinimalPasswordScore = 2 @Composable -fun SecureAppWithPassword(navController: NavController, settingsController: SettingsController) { +fun SecureAppWithPassword(onSelectPasswordAsAuthenticationMode: (String) -> Unit, onBack: () -> Unit) { var password by remember { mutableStateOf("") } var repeatedPassword by remember { mutableStateOf("") } var passwordScore by remember { mutableStateOf(0) } val focusRequester = FocusRequester.Default - val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() AnimatedElevationScaffold( topBarTitle = stringResource(R.string.settings_password_headline), navigationMode = NavigationBarMode.Back, - onBack = { navController.popBackStack() }, + onBack = onBack, elevated = scrollState.value > 0, actions = {}, bottomBar = { @@ -113,10 +109,8 @@ fun SecureAppWithPassword(navController: NavController, settingsController: Sett Spacer(modifier = Modifier.weight(1f)) Button( onClick = { - coroutineScope.launch { - settingsController.onSelectPasswordAsAuthenticationMode(password) - navController.popBackStack() - } + onSelectPasswordAsAuthenticationMode(password) + onBack() }, enabled = checkPassword( password = password, @@ -180,10 +174,8 @@ fun SecureAppWithPassword(navController: NavController, settingsController: Sett score = passwordScore ) ) { - coroutineScope.launch { - settingsController.onSelectPasswordAsAuthenticationMode(password) - navController.popBackStack() - } + onSelectPasswordAsAuthenticationMode(password) + onBack() } } ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt index 3440e5ef..59b4077d 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt @@ -33,6 +33,9 @@ import androidx.compose.material.icons.outlined.OpenInBrowser import androidx.compose.material.icons.rounded.Timeline import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -49,15 +52,24 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.provideWebIntent +import de.gematik.ti.erp.app.utils.compose.shortToast @Composable fun ProductImprovementSettingsScreen( settingsController: SettingsController, - onAllowAnalytics: (Boolean) -> Unit, + onAllowAnalytics: () -> Unit, onBack: () -> Unit ) { + val toastText = stringResource(R.string.settings_tracking_disallow_info) + + val context = LocalContext.current + val analyticsState by settingsController.analyticsState + var isAnalyticsAllowed by remember( + analyticsState.analyticsAllowed + ) { mutableStateOf(analyticsState.analyticsAllowed) } + val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -73,9 +85,17 @@ fun ProductImprovementSettingsScreen( item { SpacerMedium() AnalyticsSection( - analyticsState.analyticsAllowed - ) { allow -> - onAllowAnalytics(allow) + isAnalyticsAllowed + ) { state -> + isAnalyticsAllowed = !isAnalyticsAllowed + + if (!state) { + settingsController.changeAnalyticsState(false) + settingsController.onTrackingDisallowed() + context.shortToast(toastText) + } else { + onAllowAnalytics() + } } } item { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt index cd391ee6..cc45eeab 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt @@ -18,34 +18,48 @@ package de.gematik.ti.erp.app.settings.ui -import android.accessibilityservice.AccessibilityServiceInfo -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.analytics.usecase.ChangeAnalyticsStateUseCase +import de.gematik.ti.erp.app.analytics.usecase.IsAnalyticsAllowedUseCase import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.usecase.AllowScreenshotsUseCase +import de.gematik.ti.erp.app.settings.usecase.GetScreenShotsAllowedUseCase import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import org.kodein.di.compose.rememberInstance -@Suppress("TooManyFunctions") class SettingsController( private val settingsUseCase: SettingsUseCase, + private val isAnalyticsAllowedUseCase: IsAnalyticsAllowedUseCase, + private val changeAnalyticsStateUseCase: ChangeAnalyticsStateUseCase, + getScreenShotsAllowedUseCase: GetScreenShotsAllowedUseCase, + private val allowScreenshotsUseCase: AllowScreenshotsUseCase, + private val scope: CoroutineScope, private val analytics: Analytics ) { - private val analyticsFlow = analytics.analyticsAllowed.map { SettingStatesData.AnalyticsState(it) } + private val analyticsFlow by lazy { + isAnalyticsAllowedUseCase().map { SettingStatesData.AnalyticsState(it) } + } val analyticsState @Composable get() = analyticsFlow.collectAsStateWithLifecycle(SettingStatesData.defaultAnalyticsState) + fun changeAnalyticsState(boolean: Boolean) { + scope.launch { + changeAnalyticsStateUseCase.invoke(boolean) + } + } + private val authenticationModeFlow = settingsUseCase.authenticationMode.map { SettingStatesData.AuthenticationModeState( it @@ -62,13 +76,12 @@ class SettingsController( @Composable get() = zoomFlow.collectAsStateWithLifecycle(SettingStatesData.defaultZoomState) - private val screenShotFlow = settingsUseCase.general.map { - SettingStatesData.ScreenshotState(it.screenShotsAllowed) - } + private val screenShotsAllowed = + getScreenShotsAllowedUseCase.invoke() - val screenshotState + val screenShotsState @Composable - get() = screenShotFlow.collectAsStateWithLifecycle(SettingStatesData.defaultScreenshotState) + get() = screenShotsAllowed.collectAsStateWithLifecycle(false) suspend fun onSelectDeviceSecurityAuthenticationMode() { settingsUseCase.saveAuthenticationMode( @@ -76,16 +89,8 @@ class SettingsController( ) } - suspend fun onSelectPasswordAsAuthenticationMode(password: String) { - settingsUseCase.saveAuthenticationMode(SettingsData.AuthenticationMode.Password(password = password)) - } - - suspend fun onAllowScreenshots() { - settingsUseCase.saveAllowScreenshots(true) - } - - suspend fun onDisAllowScreenshots() { - settingsUseCase.saveAllowScreenshots(false) + fun onAllowScreenshots(allow: Boolean) = scope.launch { + allowScreenshotsUseCase.invoke(allow) } suspend fun onEnableZoom() { @@ -96,50 +101,21 @@ class SettingsController( settingsUseCase.saveZoomPreference(false) } - @Requirement( - "O.Purp_5#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Enable usage analytics." - ) - fun onTrackingAllowed() { - analytics.allowAnalytics() - } - @Requirement( "O.Purp_5#4", sourceSpecification = "BSI-eRp-ePA", rationale = "Disable usage analytics." ) fun onTrackingDisallowed() { - analytics.disallowAnalytics() - } - - suspend fun onboardingSucceeded( - authenticationMode: SettingsData.AuthenticationMode, - defaultProfileName: String, - allowTracking: Boolean - ) { - settingsUseCase.onboardingSucceeded( - authenticationMode = authenticationMode, - defaultProfileName = defaultProfileName - ) - if (allowTracking) { - onTrackingAllowed() - } else { - onTrackingDisallowed() - } + analytics.setAnalyticsPreference(false) } - var showOnboarding = runBlocking { settingsUseCase.showOnboarding.first() } - var showWelcomeDrawer = runBlocking { settingsUseCase.showWelcomeDrawer } - private var insecureDevicePromptShown = false val showInsecureDevicePrompt = settingsUseCase .showInsecureDevicePrompt .map { - if (showOnboarding) { - false - } else if (!insecureDevicePromptShown) { + // onb ... + if (!insecureDevicePromptShown) { insecureDevicePromptShown = true it } else { @@ -150,43 +126,27 @@ class SettingsController( suspend fun onAcceptInsecureDevice() { settingsUseCase.acceptInsecureDevice() } - - suspend fun acceptMlKit() { - settingsUseCase.acceptMlKit() - } - - suspend fun welcomeDrawerShown() { - settingsUseCase.welcomeDrawerShown() - } - - suspend fun mainScreenTooltipsShown() { - settingsUseCase.mainScreenTooltipsShown() - } - - fun showMainScreenToolTips(): Flow = settingsUseCase.general - .map { !it.mainScreenTooltipsShown && it.welcomeDrawerShown } - - 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() - } } @Composable fun rememberSettingsController(): SettingsController { val settingsUseCase by rememberInstance() + val isAnalyticsAllowedUseCase by rememberInstance() + val changeAnalyticsStateUseCase by rememberInstance() + val getScreenShotsAllowedUseCase by rememberInstance() + val allowScreenshotsUseCase by rememberInstance() val analytics by rememberInstance() + val scope = rememberCoroutineScope() return remember { SettingsController( - settingsUseCase, - analytics + settingsUseCase = settingsUseCase, + isAnalyticsAllowedUseCase = isAnalyticsAllowedUseCase, + changeAnalyticsStateUseCase = changeAnalyticsStateUseCase, + getScreenShotsAllowedUseCase = getScreenShotsAllowedUseCase, + allowScreenshotsUseCase = allowScreenshotsUseCase, + analytics = analytics, + scope = scope ) } } @@ -213,14 +173,4 @@ object SettingStatesData { ) val defaultZoomState = ZoomState(zoomEnabled = false) - - @Immutable - data class ScreenshotState( - val screenshotsAllowed: Boolean - ) - - // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots - val defaultScreenshotState = ScreenshotState( - screenshotsAllowed = false - ) } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt index c6b5d753..32738629 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt @@ -21,32 +21,29 @@ package de.gematik.ti.erp.app.settings.ui import AccessibilitySettingsScreen import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -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 de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.demomode.DemoModeIntent import de.gematik.ti.erp.app.demomode.startAppWithDemoMode -import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.demomode.startAppWithNormalMode import de.gematik.ti.erp.app.info.BuildConfigInformation import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.navigation.Routes import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationMode -import de.gematik.ti.erp.app.utils.compose.shortToast import kotlinx.coroutines.launch object SettingsNavigationScreens { - object Settings : Route("settings") - object AccessibilitySettings : Route("settings_accessibility") - object ProductImprovementSettings : Route("settings_productImprovements") - object DeviceSecuritySettings : Route("settings_authenticationMethods") + object Settings : Routes("settings") + object AccessibilitySettings : Routes("settings_accessibility") + object ProductImprovementSettings : Routes("settings_productImprovements") + object DeviceSecuritySettings : Routes("settings_authenticationMethods") } @Suppress("LongMethod") @@ -70,6 +67,9 @@ fun SettingsNavGraph( mainNavController = mainNavController, navController = settingsNavController, buildConfig = buildConfig, + onClickDemoModeEnd = { + DemoModeIntent.startAppWithNormalMode(activity) + }, onClickDemoMode = { DemoModeIntent.startAppWithDemoMode(activity) } @@ -82,9 +82,6 @@ fun SettingsNavGraph( ) } composable(SettingsNavigationScreens.ProductImprovementSettings.route) { - val context = LocalContext.current - val disAllowAnalyticsToast = stringResource(R.string.settings_tracking_disallow_info) - ProductImprovementSettingsScreen( settingsController = settingsController, onAllowAnalytics = { @@ -100,12 +97,7 @@ fun SettingsNavGraph( rationale = "The agreement to the use of the analytics framework could be revoked. " + "But other agreements cannot be revoked, since the app could not operate properly." ) - if (!it) { - settingsController.onTrackingDisallowed() - context.shortToast(disAllowAnalyticsToast) - } else { - mainNavController.navigate(MainNavigationScreens.AllowAnalytics.path()) - } + mainNavController.navigate(MainNavigationScreens.AllowAnalytics.path()) }, onBack = { settingsNavController.popBackStack() } ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt index 94c327da..9f4bc9e1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt @@ -88,8 +88,11 @@ import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.card.model.command.UnlockMethod +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton +import de.gematik.ti.erp.app.demomode.DemoModeObserver import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.info.BuildConfigInformation @@ -102,7 +105,6 @@ import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile. import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AlertDialog -import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall @@ -117,13 +119,13 @@ import java.util.Locale @Composable fun SettingsScreen( - mainNavController: NavController, - settingsController: SettingsController + mainNavController: NavController ) { val buildConfig by rememberInstance() val settingsNavController = rememberNavController() + val settingsController = rememberSettingsController() var previousNavEntry by remember { mutableStateOf("settings") } - trackNavigationChanges(settingsNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(settingsNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by settingsNavController.navigationModeState(SettingsNavigationScreens.Settings.route) SettingsNavGraph( @@ -140,9 +142,13 @@ fun SettingsScreenWithScaffold( mainNavController: NavController, navController: NavController, buildConfig: BuildConfigInformation, + onClickDemoModeEnd: () -> Unit, onClickDemoMode: () -> Unit ) { val context = LocalContext.current + val demoModeObserver = LocalActivity.current as? DemoModeObserver + val isDemomode = demoModeObserver?.isDemoMode() ?: false + val profilesController = rememberProfilesController() val profilesState by profilesController.getProfilesState() @@ -174,23 +180,26 @@ fun SettingsScreenWithScaffold( ProfileSection(profilesState, mainNavController) SettingsDivider() } - item { - HealthCardSection( - onClickUnlockEgk = { unlockMethod -> - mainNavController.navigate( - MainNavigationScreens.UnlockEgk.path( - unlockMethod = unlockMethod + if (!isDemomode) { + item { + HealthCardSection( + onClickUnlockEgk = { unlockMethod -> + mainNavController.navigate( + MainNavigationScreens.UnlockEgk.path( + unlockMethod = unlockMethod + ) ) - ) - }, - onClickOrderHealthCard = { - mainNavController.navigate(MainNavigationScreens.OrderHealthCard.path()) - } - ) - SettingsDivider() + }, + onClickOrderHealthCard = { + mainNavController.navigate(MainNavigationScreens.OrderHealthCard.path()) + } + ) + SettingsDivider() + } } item { GlobalSettingsSection( + isDemomode = isDemomode, onClickAccessibilitySettings = { navController.navigate(SettingsNavigationScreens.AccessibilitySettings.path()) }, @@ -200,6 +209,7 @@ fun SettingsScreenWithScaffold( onClickDeviceSecuritySettings = { navController.navigate(SettingsNavigationScreens.DeviceSecuritySettings.path()) }, + onClickDemoModeEnd = onClickDemoModeEnd, onClickDemoMode = onClickDemoMode ) SettingsDivider() @@ -229,9 +239,11 @@ fun SettingsScreenWithScaffold( @Composable fun GlobalSettingsSection( + isDemomode: Boolean, onClickAccessibilitySettings: () -> Unit, onClickProductImprovementSettings: () -> Unit, onClickDeviceSecuritySettings: () -> Unit, + onClickDemoModeEnd: () -> Unit, onClickDemoMode: () -> Unit ) { @@ -246,11 +258,20 @@ fun GlobalSettingsSection( top = PaddingDefaults.Medium ) ) - LabelButton( - icon = painterResource(R.drawable.magic_wand_filled), - stringResource(R.string.demo_mode_settings_title) - ) { - onClickDemoMode() + if (isDemomode) { + LabelButton( + icon = painterResource(R.drawable.magic_wand_filled), + stringResource(R.string.demo_mode_settings_end_title) + ) { + onClickDemoModeEnd() + } + } else { + LabelButton( + icon = painterResource(R.drawable.magic_wand_filled), + stringResource(R.string.demo_mode_settings_title) + ) { + onClickDemoMode() + } } LabelButton( Icons.Outlined.AccessibilityNew, @@ -594,7 +615,7 @@ private fun LabelButton( } @Composable -private fun LabelButton( +fun LabelButton( icon: Painter, text: String, modifier: Modifier = Modifier, diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt index add03031..20b66a66 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt @@ -20,7 +20,6 @@ package de.gematik.ti.erp.app.settings.usecase import android.app.KeyguardManager import android.content.Context -import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.profiles.usecase.sanitizedProfileName import de.gematik.ti.erp.app.settings.GeneralSettings @@ -32,13 +31,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map 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).atStartOfDayIn(TimeZone.UTC) +// TODO: Break into smaller usecases class SettingsUseCase( private val context: Context, private val settingsRepository: SettingsRepository @@ -64,7 +58,6 @@ class SettingsUseCase( } // end::ShowInsecureDevicePrompt[] - val showOnboarding = settingsRepository.general.map { it.onboardingShownIn == null } // TODO Move to Mainscreen val showWelcomeDrawer = settingsRepository.general.map { !it.welcomeDrawerShown } suspend fun welcomeDrawerShown() { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt new file mode 100644 index 00000000..c81fa9ea --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/datasource/local/TimeoutsLocalDataSource.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +@file:Suppress("MagicNumber") + +package de.gematik.ti.erp.app.timeouts.datasource.local + +import android.content.SharedPreferences +import androidx.core.content.edit +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.INACTIVITY_TIMER_ENUM +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.INACTIVITY_TIMER_VALUE +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.PAUSE_TIMER_ENUM +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.PAUSE_TIMER_VALUE +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultInactivityMetric +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultInactivityValue +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultPauseValue +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.ExperimentalTime +import kotlin.time.toDuration + +object TimeoutConstant { + const val INACTIVITY_TIMER_VALUE = "INACTIVITY_TIMER" + const val INACTIVITY_TIMER_ENUM = "INACTIVITY_TIMER_ENUM" + const val PAUSE_TIMER_VALUE = "PAUSE_TIMER" + const val PAUSE_TIMER_ENUM = "PAUSE_TIMER_ENUM" + const val defaultInactivityValue = 10 + const val defaultPauseValue = 30 + val defaultInactivityMetric = DurationUnit.MINUTES + val defaultPauseMetric = DurationUnit.SECONDS +} + +class TimeoutsLocalDataSource( + private val sharedPreferences: SharedPreferences +) { + fun setDefaultTimeouts() { + sharedPreferences.edit { + putInt(INACTIVITY_TIMER_VALUE, defaultInactivityValue) + apply() + } + sharedPreferences.edit { + putInt(PAUSE_TIMER_VALUE, defaultPauseValue) + apply() + } + sharedPreferences.edit { + putString(INACTIVITY_TIMER_ENUM, defaultInactivityMetric.name) + apply() + } + sharedPreferences.edit { + putString(INACTIVITY_TIMER_ENUM, defaultInactivityMetric.name) + apply() + } + } + + fun setInactivityTimer(duration: Duration) { + val (value, enum) = "$duration".durationEnumPair() + sharedPreferences.edit { + putInt(INACTIVITY_TIMER_VALUE, value.toInt()) + apply() + } + sharedPreferences.edit { + putString(INACTIVITY_TIMER_ENUM, enum.name) + apply() + } + } + + fun setPauseTimer(duration: Duration) { + val (value, enum) = "$duration".durationEnumPair() + sharedPreferences.edit().apply { + putInt(PAUSE_TIMER_VALUE, value.toInt()) + apply() + } + sharedPreferences.edit().apply { + putString(PAUSE_TIMER_ENUM, enum.name) + apply() + } + } + + fun getInactivityTimeout(): Duration? { + val enum = sharedPreferences.getString(INACTIVITY_TIMER_ENUM, null) + val value = sharedPreferences.getInt(INACTIVITY_TIMER_VALUE, 0) + return (value to enum).toDuration() + } + + fun getPauseTimeout(): Duration? { + val enum = sharedPreferences.getString(PAUSE_TIMER_ENUM, null) + val value = sharedPreferences.getInt(PAUSE_TIMER_VALUE, 0) + return (value to enum).toDuration() + } + + companion object { + private fun String.durationEnumPair(): Pair { + val regex = Regex("(\\d+)([hms])") + val matchResults = regex.findAll(this) + var value = "" + var unit = DurationEnum.SECONDS + for (result in matchResults) { + value = result.groupValues[1] // Extract the numeric value + unit = DurationEnum.extractedUnit(result.groupValues[2]) // Extract the unit (h, m, or s) + } + return value to unit + } + + @OptIn(ExperimentalTime::class) + fun Pair.toDuration(): Duration? { + this.first?.let { nonNullValue -> + this.second?.let { nonNullEnum -> + val durationUnit = DurationUnit.valueOf(nonNullEnum) + return Duration.convert( + nonNullValue.toDouble(), + durationUnit, + durationUnit + ).toDuration(durationUnit) + } + } + return null + } + + /** + * Allowed enums on the debug screen + */ + enum class DurationEnum { + UNSPECIFIED, + SECONDS, + MINUTES, + HOURS; + + companion object { + fun extractedUnit(input: String) = + when (input) { + "h" -> HOURS + "m" -> MINUTES + "s" -> SECONDS + else -> UNSPECIFIED + } + } + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt new file mode 100644 index 00000000..c24b3595 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/di/TimeoutsSharedPrefsModule.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.di + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsScreenViewModel +import de.gematik.ti.erp.app.timeouts.presentation.DefaultTimeoutsScreenViewModel +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository +import de.gematik.ti.erp.app.timeouts.repository.DefaultTimeoutRepository +import de.gematik.ti.erp.app.timeouts.usecase.GetInactivityMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.GetPauseMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.SetInactivityMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.SetPauseMetricUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +private const val ENCRYPTED_PREFS_FILE_NAME = "Encrypted_shared_prefs" +private const val ENCRYPTED_PREFS_MASTER_KEY_ALIAS = "ENCRYPTED_PREFS_MASTER_KEY_ALIAS" + +private const val ENCRYPTED_SHARED_PREFS_TAG = "EncryptedSharedPrefsTag" + +val timeoutsSharedPrefsModule = DI.Module("sharedPrefsModule") { + bindSingleton { + val context = instance() + val masterKey = MasterKey.Builder(context, ENCRYPTED_PREFS_MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + masterKey + } + bindSingleton(ENCRYPTED_SHARED_PREFS_TAG) { + val context = instance() + val masterKey = instance() + + EncryptedSharedPreferences.create( + context, + ENCRYPTED_PREFS_FILE_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + bindProvider { + val sharedPreferences = instance(ENCRYPTED_SHARED_PREFS_TAG) + TimeoutsLocalDataSource(sharedPreferences) + } + bindProvider { + DefaultTimeoutRepository(instance()).also { + // set the initial timeouts for the app + if (!it.areTimeoutsExisting()) { + it.setDefaultTimeouts() + } + } + } + bindProvider { GetInactivityMetricUseCase(instance()) } + bindProvider { GetPauseMetricUseCase(instance()) } + bindProvider { SetInactivityMetricUseCase(instance()) } + bindProvider { SetPauseMetricUseCase(instance()) } + bindProvider { + DefaultTimeoutsScreenViewModel(instance(), instance(), instance(), instance()) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt new file mode 100644 index 00000000..78e1a62f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/presentation/TimeoutsScreenViewModel.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultInactivityValue +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultPauseValue +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.Error +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.InactivityError +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.NoError +import de.gematik.ti.erp.app.timeouts.presentation.TimeoutsError.PauseError +import de.gematik.ti.erp.app.timeouts.usecase.GetInactivityMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.GetPauseMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.SetInactivityMetricUseCase +import de.gematik.ti.erp.app.timeouts.usecase.SetPauseMetricUseCase +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlin.time.Duration + +abstract class TimeoutsScreenViewModel : ViewModel() { + + abstract val inactivityMetricDuration: StateFlow + abstract val pauseMetricDuration: StateFlow + + abstract val error: MutableSharedFlow + abstract suspend fun load() + + abstract fun emitNoError() + + open fun setInactivityMetric( + value: String, + unit: DurationEnum + ) { + viewModelScope.launch { load() } + emitNoError() + } + + open fun setPauseMetric( + value: String, + unit: DurationEnum + ) { + viewModelScope.launch { load() } + emitNoError() + } + + open fun resetToDefaultMetrics() { + viewModelScope.launch { load() } + emitNoError() + } +} + +enum class TimeoutsError { + InactivityError, PauseError, Error, NoError +} + +class DefaultTimeoutsScreenViewModel( + private val getInactivityMetricUseCase: GetInactivityMetricUseCase, + private val getPauseMetricUseCase: GetPauseMetricUseCase, + private val setInactivityMetricUseCase: SetInactivityMetricUseCase, + private val setPauseMetricUseCase: SetPauseMetricUseCase +) : TimeoutsScreenViewModel() { + + private val inactivityMetric = MutableStateFlow(Duration.ZERO) + + private val pauseMetric = MutableStateFlow(Duration.ZERO) + + init { + viewModelScope.launch { load() } + } + + override suspend fun load() { + inactivityMetric.value = getInactivityMetricUseCase() + pauseMetric.value = getPauseMetricUseCase() + } + + override val inactivityMetricDuration: StateFlow = inactivityMetric + override val pauseMetricDuration: StateFlow = pauseMetric + + override val error = MutableSharedFlow() + + override fun setInactivityMetric( + value: String, + unit: DurationEnum + ) { + runCatching { + setInactivityMetricUseCase.invoke(value = value, unit = unit) + }.fold( + onSuccess = { + // call super only in success + super.setInactivityMetric(value, unit) + }, + onFailure = { + error.tryEmit(InactivityError) + } + ) + } + + override fun setPauseMetric( + value: String, + unit: DurationEnum + ) { + runCatching { + setPauseMetricUseCase.invoke(value = value, unit = unit) + }.fold( + onSuccess = { + // call super only in success + super.setPauseMetric(value, unit) + }, + onFailure = { + error.tryEmit(PauseError) + } + ) + } + + override fun resetToDefaultMetrics() { + runCatching { + setInactivityMetricUseCase( + value = defaultInactivityValue.toString(), + unit = DurationEnum.MINUTES + ) + setPauseMetricUseCase( + value = defaultPauseValue.toString(), + unit = DurationEnum.SECONDS + ) + }.fold( + onSuccess = { + // call super only in success + super.resetToDefaultMetrics() + }, + onFailure = { + error.tryEmit(Error) + } + ) + } + + override fun emitNoError() { + error.tryEmit(NoError) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt new file mode 100644 index 00000000..d0e97903 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/repository/DefaultTimeoutRepository.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.repository + +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultInactivityMetric +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultInactivityValue +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultPauseMetric +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutConstant.defaultPauseValue +import kotlin.time.Duration +import kotlin.time.toDuration + +class DefaultTimeoutRepository( + private val localDataSource: TimeoutsLocalDataSource +) : TimeoutRepository { + override fun setDefaultTimeouts() { + localDataSource.setDefaultTimeouts() + } + + override fun areTimeoutsExisting(): Boolean = + (localDataSource.getPauseTimeout() != null && localDataSource.getInactivityTimeout() != null) + + override fun changeInactivityTimeout(duration: Duration) { + localDataSource.setInactivityTimer(duration) + } + + override fun changePauseTimeout(duration: Duration) { + localDataSource.setPauseTimer(duration) + } + + override fun getInactivityTimeout(): Duration = + localDataSource.getInactivityTimeout() ?: defaultInactivityValue.toDuration(defaultInactivityMetric) + + override fun getPauseTimeout(): Duration = + localDataSource.getPauseTimeout() ?: defaultPauseValue.toDuration(defaultPauseMetric) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt new file mode 100644 index 00000000..0e6c5773 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetInactivityMetricUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.usecase + +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository + +class GetInactivityMetricUseCase( + private val repository: TimeoutRepository +) { + operator fun invoke() = repository.getInactivityTimeout() +} diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt similarity index 73% rename from app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt index 2bb62a48..d66ed2e3 100644 --- a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/GetPauseMetricUseCase.kt @@ -16,12 +16,12 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.timeouts.usecase -import de.gematik.ti.erp.app.Route +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository -object DebugScreenNavigation { - object DebugMain : Route("DebugMain") - object DebugRedeemWithoutFD : Route("DebugRedeemWithoutFD") - object DebugPKV : Route("DebugPKV") +class GetPauseMetricUseCase( + private val repository: TimeoutRepository +) { + operator fun invoke() = repository.getPauseTimeout() } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt new file mode 100644 index 00000000..31c60b61 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetInactivityMetricUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.usecase + +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.toDuration +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository + +class SetInactivityMetricUseCase( + private val repository: TimeoutRepository +) { + operator fun invoke( + value: String, + unit: DurationEnum + ): Result = (value.toIntOrNull() to unit.name) + .toDuration()?.let { + repository.changeInactivityTimeout(duration = it) + Result.success(Unit) + } ?: run { + Result.failure(IllegalStateException("value cannot be set, unreal values")) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt new file mode 100644 index 00000000..b3b0d764 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/timeouts/usecase/SetPauseMetricUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.usecase + +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.DurationEnum +import de.gematik.ti.erp.app.timeouts.datasource.local.TimeoutsLocalDataSource.Companion.toDuration +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository + +class SetPauseMetricUseCase( + private val repository: TimeoutRepository +) { + operator fun invoke( + value: String, + unit: DurationEnum + ): Result = (value.toIntOrNull() to unit.name).toDuration()?.let { + repository.changePauseTimeout(duration = it) + Result.success(Unit) + } ?: run { + Result.failure(IllegalStateException("value cannot be set, unreal values")) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt index c4412049..c92ddd9f 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt @@ -56,10 +56,10 @@ import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.Route -import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChangesAsync import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.navigation.Routes 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 @@ -79,10 +79,10 @@ import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import org.kodein.di.compose.rememberInstance object TroubleShootingNavigation { - object TroubleshootingPageA : Route("troubleShooting") - object TroubleshootingPageB : Route("troubleShooting_readCardHelp1") - object TroubleshootingPageC : Route("troubleShooting_readCardHelp2") - object TroubleshootingNoSuccessPage : Route("troubleShooting_readCardHelp3") + object TroubleshootingPageA : Routes("troubleShooting") + object TroubleshootingPageB : Routes("troubleShooting_readCardHelp1") + object TroubleshootingPageC : Routes("troubleShooting_readCardHelp2") + object TroubleshootingNoSuccessPage : Routes("troubleShooting_readCardHelp3") } @Composable @@ -93,7 +93,7 @@ fun TroubleShootingScreen( val navController = rememberNavController() val buildConfig by rememberInstance() var previousNavEntry by remember { mutableStateOf("troubleShooting") } - trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChangesAsync(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, startDestination = TroubleShootingNavigation.TroubleshootingPageA.path() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt new file mode 100644 index 00000000..7791170a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/BiometricPromptBuilder.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.userauthentication.observer + +import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import de.gematik.ti.erp.app.Requirement +import io.github.aakira.napier.Napier + +@Requirement( + "A_21584", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Only biometric means provided by the operating system are used." +) +open class BiometricPromptBuilder(val activity: AppCompatActivity) { + private val executor = ContextCompat.getMainExecutor(activity) + private val biometricManager = BiometricManager.from(activity) + private val authenticators = fetchAuthenticators(biometricManager) + + private fun authenticationCallback( + onSuccess: () -> Unit, + onFailure: () -> Unit, + onError: () -> Unit + ): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Napier.i( + tag = "BiometricPrompt", + message = "authentication failed" + ) + onFailure() + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Napier.i( + tag = "BiometricPrompt", + message = "authentication error $errString" + ) + onError() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Napier.i( + tag = "BiometricPrompt", + message = "authentication success $result" + ) + onSuccess() + } + } + + fun buildPromptInfo( + title: String, + description: String = "", + negativeButton: String + ): BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setDescription(description) + .apply { + if ((authenticators and BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { + setNegativeButtonText(negativeButton) + } + }.setAllowedAuthenticators(authenticators) + .build() + + fun buildBiometricPrompt( + onSuccess: () -> Unit, + onFailure: () -> Unit, + onError: () -> Unit + ): BiometricPrompt = BiometricPrompt(activity, executor, authenticationCallback(onSuccess, onFailure, onError)) + + @Requirement( + "A_21582", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Selection of the best available authentication option on the device." + ) + @Suppress("ReturnCount") + private fun fetchAuthenticators(biometricManager: BiometricManager): Int { + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == + BiometricManager.BIOMETRIC_SUCCESS || + biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + ) { + return BiometricManager.Authenticators.BIOMETRIC_STRONG + } + + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == + BiometricManager.BIOMETRIC_SUCCESS || + biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + ) { + return BiometricManager.Authenticators.BIOMETRIC_WEAK + } + if (biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == + BiometricManager.BIOMETRIC_SUCCESS || + biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED + ) { + return BiometricManager.Authenticators.DEVICE_CREDENTIAL + } + + return if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) { + BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK + } else { + BiometricManager.Authenticators.DEVICE_CREDENTIAL + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt similarity index 72% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt index caf285f9..2d66d744 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/InactivityTimeoutObserver.kt @@ -16,16 +16,16 @@ * */ -package de.gematik.ti.erp.app.userauthentication.ui +package de.gematik.ti.erp.app.userauthentication.observer import androidx.compose.runtime.Stable import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.Lifecycle.Event.ON_CREATE import androidx.lifecycle.Lifecycle.Event.ON_START -import androidx.lifecycle.Lifecycle.Event.ON_STOP import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.timeouts.repository.TimeoutRepository import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.model.SettingsData.AuthenticationMode.Password import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase @@ -33,10 +33,9 @@ import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.channelFlow @@ -47,22 +46,19 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import java.time.Duration @Stable sealed class AuthenticationModeAndMethod { - object None : AuthenticationModeAndMethod() - object Authenticated : AuthenticationModeAndMethod() + data object None : AuthenticationModeAndMethod() + data object Authenticated : AuthenticationModeAndMethod() data class AuthenticationRequired(val method: SettingsData.AuthenticationMode, val nrOfFailedAuthentications: Int) : AuthenticationModeAndMethod() } -private val InactivityTimeout = Duration.ofMinutes(10) -private val PauseTimeout = Duration.ofSeconds(30) -private const val ResetTimeout = -1L +private const val RESET_TIMEOUT = -1L // tag::AuthenticationUseCase[] @Requirement( @@ -70,9 +66,10 @@ private const val ResetTimeout = -1L sourceSpecification = "BSI-eRp-ePA", rationale = "A Timer is used to measure the time a user is inactive. Every user interaction resets the timer." ) -class AuthenticationUseCase( +class InactivityTimeoutObserver( private val settingsUseCase: SettingsUseCase, - dispatcher: CoroutineDispatcher = Dispatchers.IO + private val timeoutRepo: TimeoutRepository, + dispatcher: CoroutineDispatcher = Dispatchers.Default ) : LifecycleEventObserver { private enum class Lifecycle { Created, Started, Running, Paused @@ -82,7 +79,9 @@ class AuthenticationUseCase( private val authRequired = MutableStateFlow(false) private val lifecycle = MutableStateFlow(Lifecycle.Created) - private val timerChannel = Channel(Channel.CONFLATED) + + // Replay is required so that it keeps a buffer if the app goes in the background + private val inactivityTimerChannel = MutableSharedFlow(replay = 1) private var unspecifiedAuthentication = false @@ -97,14 +96,14 @@ class AuthenticationUseCase( when (lifecycle) { Lifecycle.Created -> { - this@AuthenticationUseCase.authRequired.value = when (authenticationMode) { + this@InactivityTimeoutObserver.authRequired.value = when (authenticationMode) { SettingsData.AuthenticationMode.Unspecified -> false else -> true } - this@AuthenticationUseCase.lifecycle.value = Lifecycle.Running + this@InactivityTimeoutObserver.lifecycle.value = Lifecycle.Running } Lifecycle.Started -> { - this@AuthenticationUseCase.lifecycle.value = Lifecycle.Running + this@InactivityTimeoutObserver.lifecycle.value = Lifecycle.Running } Lifecycle.Running -> { when (authenticationMode) { @@ -129,38 +128,33 @@ class AuthenticationUseCase( val authenticationModeAndMethod: Flow = channelFlow { launch { - var currentTimeout: Long = ResetTimeout - timerChannel - .receiveAsFlow() + var currentTimeout: Long = RESET_TIMEOUT + inactivityTimerChannel .filter { timeout -> currentTimeout <= 0 || timeout <= currentTimeout } .collectLatest { timeout -> currentTimeout = timeout if (timeout > 0) { - Napier.d { "Restarted inactivity timer for ${Duration.ofMillis(timeout)}" } + Napier.i { "Restarted inactivity timer for ${Duration.ofMillis(timeout)}" } delay(timeout) requireAuthentication() - currentTimeout = ResetTimeout + currentTimeout = RESET_TIMEOUT } else { - timerChannel.send(InactivityTimeout.toMillis()) + inactivityTimerChannel.tryEmit(timeoutRepo.getInactivityTimeout().inWholeMilliseconds) } } } - Napier.d { "Started authentication flow" } - authenticationFlow() .collect { if (it == AuthenticationModeAndMethod.Authenticated) { - timerChannel.send(InactivityTimeout.toMillis()) + inactivityTimerChannel.tryEmit(timeoutRepo.getInactivityTimeout().inWholeMilliseconds) } - - Napier.d { "Current authentication mode $it" } - send(it) } }.flowOn(dispatcher) + // need replay for buffer .shareIn(scope = scope, started = SharingStarted.Lazily, replay = 1) // end::AuthenticationUseCase[] @@ -170,7 +164,7 @@ class AuthenticationUseCase( }.first() fun resetInactivityTimer() { - timerChannel.trySendBlocking(InactivityTimeout.toMillis()) + inactivityTimerChannel.tryEmit(timeoutRepo.getInactivityTimeout().inWholeMilliseconds) } fun authenticated() { @@ -179,16 +173,17 @@ class AuthenticationUseCase( private fun requireAuthentication() { if (!unspecifiedAuthentication) { + Napier.i { "Authentication required" } authRequired.value = true - Napier.d { "Authentication required" } } } - private fun requireAuthentication(inMillis: Long) { - val result = timerChannel.trySendBlocking(inMillis) - if (result.isFailure) { - requireAuthentication() - } + /** + * This [forceRequireAuthentication] is called when the pause timeout has run + */ + fun forceRequireAuthentication() { + Napier.i { "Authentication required" } + authRequired.value = true } suspend fun incrementNumberOfAuthenticationFailures() = @@ -197,29 +192,19 @@ class AuthenticationUseCase( suspend fun resetNumberOfAuthenticationFailures() = settingsUseCase.resetNumberOfAuthenticationFailures() - @Requirement( - "O.Auth_7", - "O.Plat_12", - sourceSpecification = "BSI-eRp-ePA", - rationale = "The LifeCicleState of the app is monitored. If the app stopped, the authentication " + - "process starts after a delay of 30 seconds. We opted for this delay for usability reasons, " + - "as selecting the profile picture and external authentication requires pausing the app." - ) override fun onStateChanged(source: LifecycleOwner, event: Event) { - Napier.d { "Authentication lifecycle event state: $event" } + Napier.i { "Application lifecycle event state: $event" } when (event) { ON_CREATE -> lifecycle.value = Lifecycle.Created ON_START -> { if (lifecycle.value != Lifecycle.Created) { lifecycle.value = Lifecycle.Started } - timerChannel.trySendBlocking(ResetTimeout) + inactivityTimerChannel.tryEmit(RESET_TIMEOUT) } - ON_STOP -> { - lifecycle.value = Lifecycle.Paused - requireAuthentication(PauseTimeout.toMillis()) + else -> { + // Pause timeout is being handled in the [BaseActivity] } - else -> {} } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt new file mode 100644 index 00000000..c5a71b98 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/observer/ProcessLifecycleObserver.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.userauthentication.observer + +import androidx.lifecycle.ProcessLifecycleOwner + +/** + * [ProcessLifecycleObserver] is a singleton class that oversees the processes that are observing the app lifecycle + */ +class ProcessLifecycleObserver( + private val processLifecycleOwner: ProcessLifecycleOwner.Companion, + private val inactivityTimeoutObserver: InactivityTimeoutObserver +) { + fun observeForInactivity() { + processLifecycleOwner.get().lifecycle.apply { + addObserver(inactivityTimeoutObserver) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt similarity index 90% rename from app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt index 6a81e642..a695687a 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPromptWrapper.kt @@ -15,9 +15,11 @@ * limitations under the Licence. * */ +@file:Suppress("ReturnCount") package de.gematik.ti.erp.app.userauthentication.ui +import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.compose.runtime.Composable @@ -25,7 +27,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.utils.compose.shortToast @@ -38,7 +39,7 @@ import de.gematik.ti.erp.app.utils.compose.shortToast rationale = "Only biometric means provided by the operating system are used." ) @Composable -fun BiometricPrompt( +fun BiometricPromptWrapper( title: String, description: String, negativeButton: String, @@ -47,10 +48,10 @@ fun BiometricPrompt( onAuthenticationError: () -> Unit, onAuthenticationSoftError: () -> Unit ) { - val activity = LocalActivity.current as FragmentActivity + val activity = LocalActivity.current as? AppCompatActivity val context = LocalContext.current - val executor = remember { ContextCompat.getMainExecutor(activity) } + val executor = remember { ContextCompat.getMainExecutor(context) } val biometricManager = remember { BiometricManager.from(context) } val callback = remember { @@ -98,19 +99,16 @@ fun BiometricPrompt( if ((secureOption and BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { setNegativeButtonText(negativeButton) } - }.setAllowedAuthenticators( - secureOption - ) + }.setAllowedAuthenticators(secureOption) .build() } - val biometricPrompt = remember { BiometricPrompt(activity, executor, callback) } + val biometricPrompt = activity?.let { BiometricPrompt(it, executor, callback) } DisposableEffect(biometricPrompt) { - biometricPrompt.authenticate(promptInfo) - + biometricPrompt?.authenticate(promptInfo) onDispose { - biometricPrompt.cancelAuthentication() + biometricPrompt?.cancelAuthentication() } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt index 296298fc..9c1e88f4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt @@ -20,49 +20,54 @@ package de.gematik.ti.erp.app.userauthentication.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember -import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.userauthentication.observer.AuthenticationModeAndMethod +import de.gematik.ti.erp.app.userauthentication.observer.InactivityTimeoutObserver +import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationStateData.AuthenticationState import kotlinx.coroutines.flow.map import org.kodein.di.compose.rememberInstance class AuthenticationController( - private val authUseCase: AuthenticationUseCase + private val inactivityTimeoutObserver: InactivityTimeoutObserver ) { private val authenticationFlow = - authUseCase.authenticationModeAndMethod.map { - when (it) { - AuthenticationModeAndMethod.None, - AuthenticationModeAndMethod.Authenticated -> AuthenticationStateData.AuthenticationState( - SettingsData.AuthenticationMode.Unspecified, - 0 - ) - is AuthenticationModeAndMethod.AuthenticationRequired -> AuthenticationStateData.AuthenticationState( - it.method, - it.nrOfFailedAuthentications - ) + inactivityTimeoutObserver.authenticationModeAndMethod + .map { + when (it) { + AuthenticationModeAndMethod.None, + AuthenticationModeAndMethod.Authenticated -> AuthenticationState( + SettingsData.AuthenticationMode.Unspecified, + 0 + ) + + is AuthenticationModeAndMethod.AuthenticationRequired -> AuthenticationState( + it.method, + it.nrOfFailedAuthentications + ) + } } - } val authenticationState @Composable - get() = authenticationFlow.collectAsStateWithLifecycle(AuthenticationStateData.defaultAuthenticationState) + get() = authenticationFlow.collectAsState(AuthenticationStateData.defaultAuthenticationState) suspend fun isPasswordValid(password: String): Boolean = - authUseCase.isPasswordValid(password) + inactivityTimeoutObserver.isPasswordValid(password) suspend fun onAuthenticated() { - authUseCase.resetNumberOfAuthenticationFailures() - authUseCase.authenticated() + inactivityTimeoutObserver.resetNumberOfAuthenticationFailures() + inactivityTimeoutObserver.authenticated() } - suspend fun onFailedAuthentication() = authUseCase.incrementNumberOfAuthenticationFailures() + suspend fun onFailedAuthentication() = inactivityTimeoutObserver.incrementNumberOfAuthenticationFailures() } @Composable fun rememberAuthenticationController(): AuthenticationController { - val authenticationUseCase by rememberInstance() - return remember { AuthenticationController(authenticationUseCase) } + val inactivityTimeoutObserver by rememberInstance() + return remember { AuthenticationController(inactivityTimeoutObserver) } } object AuthenticationStateData { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt index 5f1872ee..c0231bf1 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.userauthentication.ui +import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -64,13 +65,19 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.ui.PasswordTextField import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.theme.PaddingDefaults.Medium +import de.gematik.ti.erp.app.userauthentication.observer.BiometricPromptBuilder import de.gematik.ti.erp.app.utils.compose.AlertDialog import de.gematik.ti.erp.app.utils.compose.ClickableTaggedText import de.gematik.ti.erp.app.utils.compose.HintCard @@ -87,23 +94,60 @@ import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import kotlinx.coroutines.launch import java.util.Locale + @Suppress("LongMethod") @Composable fun UserAuthenticationScreen() { - val authentication = rememberAuthenticationController() + val activity = LocalActivity.current + + val biometricPromptBuilder = remember { BiometricPromptBuilder(activity as AppCompatActivity) } + + // clear underlying text input focus + val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() + + val authentication = rememberAuthenticationController() + val authenticationState by authentication.authenticationState + val navBarInsetsPadding = WindowInsets.systemBars.asPaddingValues() + var showAuthPrompt by remember { mutableStateOf(false) } var showError by remember { mutableStateOf(false) } var initiallyHandledAuthPrompt by rememberSaveable { mutableStateOf(false) } - val authenticationState by authentication.authenticationState - val navBarInsetsPadding = WindowInsets.systemBars.asPaddingValues() - val paddingModifier = if (navBarInsetsPadding.calculateBottomPadding() <= PaddingDefaults.Medium) { - Modifier.statusBarsPadding() - } else { - Modifier.systemBarsPadding() + + val infoBuilder = biometricPromptBuilder.buildPromptInfo( + title = stringResource(R.string.auth_prompt_headline), + negativeButton = stringResource(R.string.auth_prompt_cancel) + ) + + val prompt = remember(biometricPromptBuilder) { + biometricPromptBuilder.buildBiometricPrompt( + onSuccess = { + scope.launch { authentication.onAuthenticated() } + showAuthPrompt = false + }, + onFailure = { + showAuthPrompt = false + }, + onError = { + scope.launch { authentication.onFailedAuthentication() } + showAuthPrompt = false + showError = true + } + ) } - // clear underlying text input focus - val focusManager = LocalFocusManager.current + + LaunchedEffect(showAuthPrompt) { + activity.lifecycleScope.launch { + activity.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + if (authenticationState.authenticationMethod !is SettingsData.AuthenticationMode.Password && + showAuthPrompt + ) { + prompt.authenticate(infoBuilder) + } + } + } + } + LaunchedEffect(Unit) { focusManager.clearFocus(true) if (!initiallyHandledAuthPrompt && authenticationState.nrOfAuthFailures == 0) { @@ -118,12 +162,17 @@ fun UserAuthenticationScreen() { .padding(it) .fillMaxSize() .verticalScroll(rememberScrollState()) - .then(paddingModifier) + .then( + when { + navBarInsetsPadding.calculateBottomPadding() <= Medium -> Modifier.statusBarsPadding() + else -> Modifier.systemBarsPadding() + } + ) ) { Row( modifier = Modifier - .padding(top = PaddingDefaults.Medium) - .padding(horizontal = PaddingDefaults.Medium) + .padding(top = Medium) + .padding(horizontal = Medium) .align(Alignment.Start), verticalAlignment = Alignment.CenterVertically ) { @@ -160,53 +209,29 @@ fun UserAuthenticationScreen() { null, modifier = Modifier .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), + .padding(horizontal = Medium), contentScale = ContentScale.FillWidth ) } } } - if (showAuthPrompt) { - when (authenticationState.authenticationMethod) { - is SettingsData.AuthenticationMode.Password -> - PasswordPrompt( - authentication, - onAuthenticated = { - showAuthPrompt = false - scope.launch { authentication.onAuthenticated() } - }, - onCancel = { - showAuthPrompt = false - }, - onAuthenticationError = { - scope.launch { authentication.onFailedAuthentication() } - showAuthPrompt = false - showError = true - } - ) - else -> - BiometricPrompt( - title = stringResource(R.string.auth_prompt_headline), - description = "", - negativeButton = stringResource(R.string.auth_prompt_cancel), - onAuthenticated = { - showAuthPrompt = false - scope.launch { authentication.onAuthenticated() } - }, - onCancel = { - showAuthPrompt = false - }, - onAuthenticationError = { - scope.launch { authentication.onFailedAuthentication() } - showAuthPrompt = false - showError = true - }, - onAuthenticationSoftError = { - scope.launch { authentication.onFailedAuthentication() } - } - ) - } + if (showAuthPrompt && authenticationState.authenticationMethod is SettingsData.AuthenticationMode.Password) { + PasswordPrompt( + authentication, + onAuthenticated = { + showAuthPrompt = false + scope.launch { authentication.onAuthenticated() } + }, + onCancel = { + showAuthPrompt = false + }, + onAuthenticationError = { + scope.launch { authentication.onFailedAuthentication() } + showAuthPrompt = false + showError = true + } + ) } } @@ -217,7 +242,7 @@ private fun AuthenticationScreenErrorContent( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), + .padding(horizontal = Medium), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(80.dp)) @@ -263,9 +288,9 @@ private fun AuthenticationScreenErrorBottomContent(state: AuthenticationStateDat .background(color = AppTheme.colors.neutral100) .padding( bottom = PaddingDefaults.Large, - start = PaddingDefaults.Medium, - end = PaddingDefaults.Medium, - top = PaddingDefaults.Medium + start = Medium, + end = Medium, + top = Medium ) .fillMaxWidth() ) { @@ -301,12 +326,12 @@ private fun AuthenticationScreenContent( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium), + .padding(horizontal = Medium), horizontalAlignment = Alignment.CenterHorizontally ) { if (state.nrOfAuthFailures > 0) { HintCard( - modifier = Modifier.padding(vertical = PaddingDefaults.Medium), + modifier = Modifier.padding(vertical = Medium), properties = HintCardDefaults.flatProperties( backgroundColor = AppTheme.colors.red100 ), diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt index e4899d16..0ba09bd4 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt @@ -52,8 +52,13 @@ fun ClickableText( val resources = LocalContext.current.resources val textData = remember(clickText, text, resources) { - val regex = Regex(clickText.text) - val splits = text.splitToSequence(regex).toMutableList() + val spacingDelimiter = " " + val regex = Regex(clickText.text.lowercase()) + val splits = text + .split(spacingDelimiter) + .joinToString(spacingDelimiter) + .splitToSequence(regex) + .toMutableList() // add the delimiter also to the list splits.add(1, clickText.text) mutableListOf().apply { @@ -61,7 +66,7 @@ fun ClickableText( if (part == clickText.text) { add( TextData( - text = clickText.text, + text = " ${clickText.text} ", tag = "$clickText-link-text-tag", onClick = { clickText.onClick() } ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt new file mode 100644 index 00000000..f8606c52 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposeTags.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +/** + * Tag used for the component [ErrorText] + */ +const val ErrorTextTag = "error-text" diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt index 1b7b1555..9d20d743 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt @@ -18,7 +18,9 @@ package de.gematik.ti.erp.app.utils.compose +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentWidth @@ -45,8 +47,10 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import de.gematik.ti.erp.app.features.R @@ -62,13 +66,13 @@ fun EditableHeaderTextField( onSaveText: (String) -> Unit ) { var isEditing by remember { mutableStateOf(false) } - - var name by remember { - mutableStateOf(text) - } + var name by remember { mutableStateOf(text) } if (isEditing) { - EditableTextField(text, textMinLength) { + EditableTextField( + text = text, + textMinLength = textMinLength + ) { onSaveText(it) isEditing = false name = it @@ -112,60 +116,54 @@ private fun EditIconButton( @OptIn(ExperimentalComposeUiApi::class) @Composable -private fun EditableTextField( +fun EditableTextField( + modifier: Modifier = Modifier, text: String, textMinLength: Int, - onSaveText: (String) -> Unit + onDoneClicked: (String) -> Unit ) { - var name by remember { - mutableStateOf(text) - } + var name by remember { mutableStateOf(TextFieldValue(text)) } val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current var isError by remember { mutableStateOf(false) } - val keyboardOption = KeyboardOptions.Default.copy( - imeAction = ImeAction.Done - ) - - val keyboardActionsDone = KeyboardActions( - onDone = { - if (!isError) { - focusManager.clearFocus() - keyboardController?.hide() - onSaveText(name.trim()) - } - } - ) - LaunchedEffect(Unit) { focusRequester.requestFocus() } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally ) { TextField( - modifier = Modifier.wrapContentWidth().focusRequester(focusRequester), + modifier = Modifier.focusRequester(focusRequester), colors = basicTextFieldColors, textStyle = AppTheme.typography.h5.copy(textAlign = TextAlign.Center), - keyboardOptions = keyboardOption, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), isError = isError, - label = { - if (isError) { - Text(stringResource(R.string.empty_scanned_prescription_name)) - } - }, value = name, - keyboardActions = keyboardActionsDone, onValueChange = { - name = it.trimStart() - isError = name.length < textMinLength - } + name = it + isError = name.text.length < textMinLength + }, + keyboardActions = KeyboardActions( + onDone = { + if (!isError) { + focusManager.clearFocus() + keyboardController?.hide() + onDoneClicked(name.text.trim()) + } + } + ) ) + AnimatedVisibility(isError) { + SpacerTiny() + ErrorText( + modifier = Modifier.testTag(ErrorTextTag), + text = stringResource(R.string.empty_scanned_prescription_name) + ) + } } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt new file mode 100644 index 00000000..c669e5ae --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ErrorText.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.AppTheme + +@Composable +fun ErrorText( + modifier: Modifier = Modifier, + text: String +) { + Text( + modifier = modifier, + text = text, + color = AppTheme.colors.red600, + style = AppTheme.typography.caption1 + ) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt new file mode 100644 index 00000000..ee4a8eb1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toggle.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun Toggle( + modifier: Modifier = Modifier, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + description: String +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .toggleable( + value = checked, + onValueChange = onCheckedChange, + role = Role.Switch, + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current + ) + .background(color = AppTheme.colors.neutral100, shape = RoundedCornerShape(16.dp)) + .fillMaxWidth() + .padding(PaddingDefaults.Medium) + .semantics(true) {}, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + description, + style = AppTheme.typography.subtitle1, + modifier = Modifier.weight(1f) + ) + SpacerSmall() + Switch( + checked = checked, + onCheckedChange = null + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt new file mode 100644 index 00000000..0ede4fae --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DialogScaffold.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.extensions + +import android.app.Dialog +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +interface DialogScaffold { + fun show(content: @Composable (Dialog) -> Unit) +} + +val LocalDialog: DialogScaffoldCompositionLocal = DialogScaffoldCompositionLocal + +object DialogScaffoldCompositionLocal { + val current: DialogScaffold + @Composable + get() = LocalContext.current as DialogScaffold +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt new file mode 100644 index 00000000..d992be7f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/SnackbarScaffold.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.extensions + +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import de.gematik.ti.erp.app.features.R + +interface SnackbarScaffold { + fun show( + text: String, + @DrawableRes icon: Int = R.drawable.ic_logo_outlined, + @ColorRes backgroundTint: Int = R.color.primary_600 + ) +} + +val LocalSnackbar: SnackbarScaffoldCompositionLocal = SnackbarScaffoldCompositionLocal + +object SnackbarScaffoldCompositionLocal { + val current: SnackbarScaffold + @Composable + get() = LocalContext.current as SnackbarScaffold +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt index 464e8d32..e4e54c83 100644 --- a/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt @@ -61,11 +61,12 @@ val vauModule = DI.Module("vauModule") { } bindSingleton { { Clock.System.now() } } bindSingleton { - { untrustedOCSPList: UntrustedOCSPList, - untrustedCertList: UntrustedCertList, - trustAnchor: X509CertificateHolder, - ocspResponseMaxAge: Duration, - timestamp: Instant -> + { + untrustedOCSPList: UntrustedOCSPList, + untrustedCertList: UntrustedCertList, + trustAnchor: X509CertificateHolder, + ocspResponseMaxAge: Duration, + timestamp: Instant -> TrustedTruststore.create( untrustedOCSPList = untrustedOCSPList, untrustedCertList = untrustedCertList, diff --git a/app/features/src/main/res/drawable/ic_finger.xml b/app/features/src/main/res/drawable/ic_finger.xml new file mode 100644 index 00000000..e7bac20f --- /dev/null +++ b/app/features/src/main/res/drawable/ic_finger.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/features/src/main/res/drawable/ic_log_mail.xml b/app/features/src/main/res/drawable/ic_log_mail.xml new file mode 100644 index 00000000..fa1711ff --- /dev/null +++ b/app/features/src/main/res/drawable/ic_log_mail.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/features/src/main/res/drawable/ic_pkv.xml b/app/features/src/main/res/drawable/ic_pkv.xml new file mode 100644 index 00000000..fc1aee03 --- /dev/null +++ b/app/features/src/main/res/drawable/ic_pkv.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/features/src/main/res/drawable/ic_prescription_refresh.xml b/app/features/src/main/res/drawable/ic_prescription_refresh.xml new file mode 100644 index 00000000..efb56b84 --- /dev/null +++ b/app/features/src/main/res/drawable/ic_prescription_refresh.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/features/src/main/res/drawable/ic_qr_code.xml b/app/features/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 00000000..6d5ebdfb --- /dev/null +++ b/app/features/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/features/src/main/res/drawable/ic_reset.xml b/app/features/src/main/res/drawable/ic_reset.xml new file mode 100644 index 00000000..edbd1145 --- /dev/null +++ b/app/features/src/main/res/drawable/ic_reset.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/features/src/main/res/raw/health_insurance_contacts.json b/app/features/src/main/res/raw/health_insurance_contacts.json index 755c8fbf..d4dd5942 100644 --- a/app/features/src/main/res/raw/health_insurance_contacts.json +++ b/app/features/src/main/res/raw/health_insurance_contacts.json @@ -22,7 +22,7 @@ "bodyPinMail": null }, { - "name": "AOK Bremen", + "name": "AOK Bremen/Bremerhaven", "healthCardAndPinPhone": "+4942117610", "healthCardAndPinMail": "info@hb.aok.de", "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", @@ -32,17 +32,6 @@ "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" }, - { - "name": "AOK Bremerhaven", - "healthCardAndPinPhone": "+49471160", - "healthCardAndPinMail": "info@hb.aok.de", - "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", - "pinUrl": null, - "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", - "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", - "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", - "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" - }, { "name": "AOK - Die Gesundheitskasse Hessen", "healthCardAndPinPhone": null, @@ -59,7 +48,7 @@ "healthCardAndPinPhone": "+498000265637", "healthCardAndPinMail": null, "healthCardAndPinUrl": null, - "pinUrl": null, + "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -78,10 +67,10 @@ }, { "name": "AOK Nordwest - Die Gesundheitskasse", - "healthCardAndPinPhone": "+498002655060", + "healthCardAndPinPhone": null, "healthCardAndPinMail": null, - "healthCardAndPinUrl": null, - "pinUrl": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", + "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -114,7 +103,7 @@ "healthCardAndPinPhone": null, "healthCardAndPinMail": null, "healthCardAndPinUrl": "https://www.aok.de/pk/rps/inhalt/die-haeufigsten-fragen-und-antworten-zum-e-rezept-4/", - "pinUrl": null, + "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -146,8 +135,8 @@ "name": "BAHN-BKK", "healthCardAndPinPhone": null, "healthCardAndPinMail": null, - "healthCardAndPinUrl": "https://www.bahn-bkk.de/egk-erezept", - "pinUrl": "https://www.bahn-bkk.de/egk-erezept", + "healthCardAndPinUrl": "https://www.bahn-bkk.de/versicherung/pin-fuer-egk/7473", + "pinUrl": "https://www.bahn-bkk.de/versicherung/pin-fuer-egk/7473", "subjectCardAndPinMail": null, "bodyCardAndPinMail": null, "subjectPinMail": null, @@ -616,7 +605,7 @@ "bodyPinMail": null }, { - "name": "BKK Verkehrsbau Union (VBU)", + "name": "mkk – meine krankenkasse", "healthCardAndPinPhone": null, "healthCardAndPinMail": "info@bkk-vbu.de", "healthCardAndPinUrl": null, diff --git a/app/features/src/main/res/values/strings.xml b/app/features/src/main/res/values/strings.xml index bef468be..156850aa 100644 --- a/app/features/src/main/res/values/strings.xml +++ b/app/features/src/main/res/values/strings.xml @@ -220,7 +220,7 @@ Ich nehme das erhöhte Risiko zur Kenntnis und möchte dennoch fortfahren. Weshalb sind Geräte mit Root-Zugriff ein potentielles Sicherheitsrisiko? Mehr erfahren - https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html + https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html Name des Profils Bitte geben Sie einen Namen für das neue Profil ein. Profilname @@ -916,4 +916,5 @@ Demo-Modus aktiviert Hier Beenden Demo-Modus aktivieren + Demo-Modus Beenden diff --git a/app/features/src/main/res/values/themes.xml b/app/features/src/main/res/values/themes.xml index af55e469..4de55388 100644 --- a/app/features/src/main/res/values/themes.xml +++ b/app/features/src/main/res/values/themes.xml @@ -9,4 +9,13 @@ @color/neutral false + + + + diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/DebugSettingsData.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/DebugSettingsData.kt new file mode 100644 index 00000000..81e1b563 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/DebugSettingsData.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.data + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.parcelize.Parcelize + +@Immutable +@Parcelize +data class DebugSettingsData( + val eRezeptServiceURL: String, + val eRezeptActive: Boolean, + val idpUrl: String, + val idpActive: Boolean, + val pharmacyServiceUrl: String, + val pharmacyServiceActive: Boolean, + val bearerToken: String, + val bearerTokenIsSet: Boolean, + val fakeNFCCapabilities: Boolean, + val cardAccessNumberIsSet: Boolean, + val multiProfile: Boolean, + val activeProfileId: ProfileIdentifier, + val virtualHealthCardCert: String, + val virtualHealthCardPrivateKey: String +) : Parcelable diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/Environment.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/Environment.kt new file mode 100644 index 00000000..3be5c794 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/data/Environment.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.data + +enum class Environment { + PU, TU, RU, RUDEV, TR +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt new file mode 100644 index 00000000..032c213a --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +import android.content.SharedPreferences +import androidx.core.content.edit +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.data.Environment + +class EndpointHelper( + private val networkPrefs: SharedPreferences +) { + + enum class EndpointUri(val original: String, val preferenceKey: String) { + BASE_SERVICE_URI( + BuildKonfig.BASE_SERVICE_URI, + "BASE_SERVICE_URI_OVERRIDE" + ), + IDP_SERVICE_URI( + BuildKonfig.IDP_SERVICE_URI, + "IDP_SERVICE_URI_OVERRIDE" + ), + PHARMACY_SERVICE_URI( + BuildKonfig.PHARMACY_SERVICE_URI, + "PHARMACY_BASE_URI_OVERRIDE" + ) + } + + val eRezeptServiceUri + get() = getUriForEndpoint(EndpointUri.BASE_SERVICE_URI) + + val idpServiceUri + get() = getUriForEndpoint(EndpointUri.IDP_SERVICE_URI) + + val pharmacySearchBaseUri + get() = getUriForEndpoint(EndpointUri.PHARMACY_SERVICE_URI) + + private fun getUriForEndpoint(uri: EndpointUri): String { + var url = uri.original + if (isUriOverridden(uri)) { + url = networkPrefs.getString( + uri.preferenceKey, + uri.original + )!! + } + if (url.last() != '/') { + url += '/' + } + return url + } + + private fun overrideSwitchKey(uri: EndpointUri): String { + return uri.preferenceKey + "_ACTIVE" + } + + fun isUriOverridden(uri: EndpointUri): Boolean { + return networkPrefs.getBoolean(overrideSwitchKey(uri), false) + } + + fun setUriOverride(uri: EndpointUri, debugUri: String, active: Boolean) { + networkPrefs.edit(commit = true) { + putBoolean(overrideSwitchKey(uri), active) + putString(uri.preferenceKey, debugUri) + } + } + + fun getCurrentEnvironment(): Environment { + return when { + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_PU && + idpServiceUri == BuildKonfig.IDP_SERVICE_URI_PU && + pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_PU -> { + Environment.PU + } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU && + idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU && + pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { + Environment.RU + } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_RU_DEV && + idpServiceUri == BuildKonfig.IDP_SERVICE_URI_RU_DEV && + pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { + Environment.RUDEV + } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TU && + idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TU && + pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { + Environment.TU + } + eRezeptServiceUri == BuildKonfig.BASE_SERVICE_URI_TR && + idpServiceUri == BuildKonfig.IDP_SERVICE_URI_TR && + pharmacySearchBaseUri == BuildKonfig.PHARMACY_SERVICE_URI_RU -> { + Environment.TR + } + else -> { + return Environment.PU + } + } + } + + fun getErpApiKey(): String { + return if (BuildKonfig.INTERNAL) { + when (getCurrentEnvironment()) { + Environment.PU -> BuildKonfig.ERP_API_KEY_GOOGLE_PU + Environment.TU -> BuildKonfig.ERP_API_KEY_GOOGLE_TU + Environment.RUDEV, Environment.RU -> BuildKonfig.ERP_API_KEY_GOOGLE_RU + Environment.TR -> BuildKonfig.ERP_API_KEY_GOOGLE_TR + } + } else { + BuildKonfig.ERP_API_KEY + } + } + + fun getIdpScope(): String { + return if (BuildKonfig.INTERNAL) { + when (getCurrentEnvironment()) { + Environment.RUDEV -> BuildKonfig.IDP_SCOPE_DEVRU + else -> BuildKonfig.IDP_DEFAULT_SCOPE + } + } else { + BuildKonfig.IDP_DEFAULT_SCOPE + } + } + + fun getPharmacyApiKey(): String { + return if (BuildKonfig.INTERNAL) { + when (getCurrentEnvironment()) { + Environment.PU -> BuildKonfig.PHARMACY_API_KEY_PU + else -> BuildKonfig.PHARMACY_API_KEY_RU + } + } else { + BuildKonfig.PHARMACY_API_KEY + } + } + + fun getTrustAnchor(): String { + return if (BuildKonfig.INTERNAL) { + when (getCurrentEnvironment()) { + Environment.PU -> BuildKonfig.APP_TRUST_ANCHOR_BASE64_PU + else -> BuildKonfig.APP_TRUST_ANCHOR_BASE64_TU + } + } else { + BuildKonfig.APP_TRUST_ANCHOR_BASE64 + } + } +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/navigation/DebugScreenNavigation.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/navigation/DebugScreenNavigation.kt new file mode 100644 index 00000000..4a63a258 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/navigation/DebugScreenNavigation.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.navigation + +import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen + +object DebugScreenNavigation { + object DebugMain : Routes("DebugMain") + object DebugRedeemWithoutFD : Routes("DebugRedeemWithoutFD") + object DebugPKV : Routes("DebugPKV") + object DebugBiometric : Routes(DebugTimeoutScreen::class::java.name) +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugLoadingButton.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugLoadingButton.kt new file mode 100644 index 00000000..c231c621 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugLoadingButton.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.AlertDialog +import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import kotlinx.coroutines.launch + +@Composable +fun DebugLoadingButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: String, + onClick: suspend () -> Unit +) { + var loading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + errorMessage?.let { error -> + AlertDialog( + onDismissRequest = { + errorMessage = null + }, + buttons = { + Button(onClick = { errorMessage = null }) { + Text("OK") + } + }, + text = { + Text(error) + } + ) + } + val scope = rememberCoroutineScope() + + Button( + modifier = modifier.fillMaxWidth(), + onClick = { + loading = true + scope.launch { + try { + onClick() + } catch (e: Exception) { + errorMessage = e.message + (e.cause?.message?.let { " - cause: $it" } ?: "") + } finally { + loading = false + } + } + }, + enabled = enabled && !loading + ) { + if (loading) { + CircularProgressIndicator(Modifier.size(24.dp), strokeWidth = 2.dp, color = AppTheme.colors.neutral600) + SpacerSmall() + } + Text(text, textAlign = TextAlign.Center) + } +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugPKV.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugPKV.kt new file mode 100644 index 00000000..4e7fbdc7 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugPKV.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberViewModel + +@Composable +fun DebugScreenPKV(onBack: () -> Unit) { + val viewModel by rememberViewModel() + val listState = rememberLazyListState() + + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + listState = listState, + topBarTitle = "Debug PKV", + onBack = onBack + ) { innerPadding -> + var invoiceBundle by remember { mutableStateOf("") } + val scope = rememberCoroutineScope() + + LazyColumn( + state = listState, + modifier = Modifier + .padding(innerPadding) + .navigationBarsPadding() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + item { + DebugCard(title = "Login state") { + val profilesController = rememberProfilesController() + val activeProfile by profilesController.getActiveProfileState() + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = { + scope.launch { + profilesController.switchToPrivateInsurance(activeProfile.id) + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Set User with ${activeProfile.name} as PKV", textAlign = TextAlign.Center) + } + } + } + } + item { + DebugCard( + title = "Invoice Bundle" + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = invoiceBundle, + label = { Text("Bundle") }, + onValueChange = { + invoiceBundle = it + }, + maxLines = 1 + ) + DebugLoadingButton( + onClick = { viewModel.saveInvoice(invoiceBundle) }, + text = "Save Invoice" + ) + } + } + } + } +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt new file mode 100644 index 00000000..14f265aa --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreen.kt @@ -0,0 +1,762 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.ui + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.RadioButton +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.debugsettings.timeout.DebugTimeoutScreen +import de.gematik.ti.erp.app.data.Environment +import de.gematik.ti.erp.app.navigation.DebugScreenNavigation +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.settings.ui.LabelButton +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AlertDialog +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.NavigationAnimation +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.navigationModeState +import kotlinx.coroutines.launch +import org.bouncycastle.util.encoders.Base64 +import org.kodein.di.bindProvider +import org.kodein.di.compose.rememberViewModel +import org.kodein.di.compose.subDI +import org.kodein.di.instance +import java.io.ByteArrayOutputStream +import java.time.LocalDateTime +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +@Composable +fun DebugCard( + modifier: Modifier = Modifier, + title: String, + onReset: (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit +) = + Card( + modifier = modifier, + shape = RoundedCornerShape(24.dp), + backgroundColor = AppTheme.colors.neutral100, + elevation = 10.dp + ) { + Box( + contentAlignment = Alignment.Center, + content = { + Column( + Modifier.padding(PaddingDefaults.Medium), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + Text( + title, + style = MaterialTheme.typography.h6, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + SpacerMedium() + content() + } + onReset?.run { + IconButton( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(PaddingDefaults.Small), + onClick = onReset + ) { + Icon(Icons.Rounded.Refresh, null) + } + } + } + ) + } + +@Composable +fun EditablePathComponentSetButton( + modifier: Modifier = Modifier, + label: String, + text: String, + active: Boolean, + onValueChange: (String, Boolean) -> Unit, + onClick: () -> Unit +) { + val color = if (active) Color.Green else Color.Red + val buttonText = if (active) "SAVED" else "SET" + EditablePathComponentWithControl( + modifier = modifier, + label = label, + textFieldValue = text, + onValueChange = onValueChange, + content = { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors(backgroundColor = color), + enabled = !active + + ) { + Text(text = buttonText) + } + } + ) +} + +@Composable +fun EditablePathComponentWithControl( + modifier: Modifier, + label: String, + textFieldValue: String, + onValueChange: (String, Boolean) -> Unit, + content: @Composable ((Boolean) -> Unit) -> Unit +) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier.fillMaxWidth()) { + TextField( + value = textFieldValue, + onValueChange = { onValueChange(it, false) }, + label = { Text(label) }, + maxLines = 3, + modifier = Modifier + .weight(1f) + .padding(end = PaddingDefaults.Medium) + ) + + content { onValueChange(textFieldValue, it) } + } +} + +@Composable +fun DebugScreen( + settingsNavController: NavController +) { + val navController = rememberNavController() + val navMode by navController.navigationModeState(DebugScreenNavigation.DebugMain.path()) + + subDI(diBuilder = { + bindProvider { + DebugSettingsViewModel( + visibleDebugTree = instance(), + endpointHelper = instance(), + cardWallUseCase = instance(), + prescriptionUseCase = instance(), + invoiceRepository = instance(), + vauRepository = instance(), + idpRepository = instance(), + idpUseCase = instance(), + profilesUseCase = instance(), + featureToggleManager = instance(), + pharmacyDirectRedeemUseCase = instance(), + dispatchers = instance() + ) + } + }) { + NavHost( + navController, + startDestination = DebugScreenNavigation.DebugMain.path() + ) { + composable(DebugScreenNavigation.DebugMain.route) { + NavigationAnimation(mode = navMode) { + DebugScreenMain( + onBack = { + settingsNavController.popBackStack() + }, + onClickDirectRedemption = { + navController.navigate(DebugScreenNavigation.DebugRedeemWithoutFD.path()) + }, + onClickPKV = { + navController.navigate(DebugScreenNavigation.DebugPKV.path()) + }, + onClickBioMetricSettings = { + navController.navigate(DebugScreenNavigation.DebugBiometric.path()) + } + ) + } + } + composable(DebugScreenNavigation.DebugRedeemWithoutFD.route) { + NavigationAnimation(mode = navMode) { + DebugScreenDirectRedeem( + onBack = { + navController.popBackStack() + } + ) + } + } + composable(DebugScreenNavigation.DebugPKV.route) { + NavigationAnimation(mode = navMode) { + DebugScreenPKV( + onBack = { + navController.popBackStack() + } + ) + } + } + + composable(DebugScreenNavigation.DebugBiometric.route) { + DebugTimeoutScreen.Content { + navController.popBackStack() + } + } + } + } +} + +@Composable +fun DebugScreenDirectRedeem(onBack: () -> Unit) { + val viewModel by rememberViewModel() + val listState = rememberLazyListState() + + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + listState = listState, + topBarTitle = "Debug Redeem", + onBack = onBack + ) { innerPadding -> + var shipmentUrl by remember { mutableStateOf("") } + var deliveryUrl by remember { mutableStateOf("") } + var onPremiseUrl by remember { mutableStateOf("") } + var message by remember { mutableStateOf("") } + var certificates by remember { mutableStateOf("") } + + LazyColumn( + state = listState, + modifier = Modifier + .padding(innerPadding) + .navigationBarsPadding() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + item { + DebugCard( + title = "Endpoints" + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = shipmentUrl, + label = { Text("Shipment URL") }, + onValueChange = { + shipmentUrl = it + } + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = deliveryUrl, + label = { Text("Delivery URL") }, + onValueChange = { + deliveryUrl = it + } + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = onPremiseUrl, + label = { Text("OnPremise URL") }, + onValueChange = { + onPremiseUrl = it + } + ) + } + } + item { + RedeemButton( + viewModel = viewModel, + url = shipmentUrl, + message = message, + certificates = certificates, + text = "Send as Shipment" + ) + RedeemButton( + viewModel = viewModel, + url = deliveryUrl, + message = message, + certificates = certificates, + text = "Send as Delivery" + ) + RedeemButton( + viewModel = viewModel, + url = onPremiseUrl, + message = message, + certificates = certificates, + text = "Send as OnPremise" + ) + } + item { + DebugCard( + title = "Message" + ) { + OutlinedTextField( + modifier = Modifier + .heightIn(max = 400.dp) + .fillMaxWidth(), + value = message, + label = { Text("Any Message") }, + onValueChange = { + message = it + } + ) + } + } + item { + DebugCard( + title = "Certificates" + ) { + OutlinedTextField( + modifier = Modifier + .heightIn(max = 400.dp) + .fillMaxWidth(), + value = certificates, + label = { Text("Certificate as PEM") }, + onValueChange = { + certificates = it + } + ) + } + } + } + } +} + +@Composable +private fun RedeemButton( + viewModel: DebugSettingsViewModel, + url: String, + message: String, + certificates: String, + text: String +) = + DebugLoadingButton( + onClick = { viewModel.redeemDirect(url = url, message = message, certificatesPEM = certificates) }, + enabled = url.isNotEmpty() && certificates.isNotEmpty(), + text = text + ) + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun DebugScreenMain( + onBack: () -> Unit, + onClickDirectRedemption: () -> Unit, + onClickPKV: () -> Unit, + onClickBioMetricSettings: () -> Unit +) { + val viewModel by rememberViewModel() + val listState = rememberLazyListState() + val modal = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + + ModalBottomSheetLayout( + sheetContent = { + EnvironmentSelector( + currentSelectedEnvironment = viewModel.getCurrentEnvironment(), + onSelectEnvironment = { viewModel.selectEnvironment(it) } + ) { + scope.launch { viewModel.saveAndRestartApp() } + } + }, + sheetState = modal + ) { + AnimatedElevationScaffold( + modifier = Modifier.testTag(TestTag.DebugMenu.DebugMenuScreen), + navigationMode = NavigationBarMode.Close, + listState = listState, + topBarTitle = "Debug Settings", + onBack = onBack + ) { innerPadding -> + + LaunchedEffect(Unit) { + viewModel.state() + } + + LazyColumn( + state = listState, + modifier = Modifier + .padding(innerPadding) + .navigationBarsPadding() + .testTag(TestTag.DebugMenu.DebugMenuContent), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), + contentPadding = PaddingValues(PaddingDefaults.Medium) + ) { + item { + DebugCard( + title = "General" + ) { + LabelButton( + icon = painterResource(R.drawable.ic_finger), + text = "Biometric settings" + ) { + onClickBioMetricSettings() + } + LabelButton( + icon = painterResource(R.drawable.ic_qr_code), + text = "Direct Redemption" + ) { + onClickDirectRedemption() + } + LabelButton( + icon = painterResource(R.drawable.ic_pkv), + text = "PKV" + ) { + onClickPKV() + } + LabelButton( + icon = painterResource(R.drawable.ic_prescription_refresh), + text = "Trigger Prescription Refresh" + ) { + viewModel.refreshPrescriptions() + } + } + } + item { + DebugCard( + title = "Card Wall" + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Fake NFC Capability", + modifier = Modifier + .weight(1f) + ) + Switch( + modifier = Modifier.testTag(TestTag.DebugMenu.FakeNFCCapabilities), + checked = viewModel.debugSettingsData.fakeNFCCapabilities, + onCheckedChange = { viewModel.allowNfc(it) } + ) + } + } + } + item { + DebugCard( + title = "Authentication" + ) { + EditablePathComponentSetButton( + label = "Bearer Token", + text = viewModel.debugSettingsData.bearerToken, + active = viewModel.debugSettingsData.bearerTokenIsSet, + onValueChange = { text, _ -> + viewModel.updateState( + viewModel.debugSettingsData.copy( + bearerToken = text, + bearerTokenIsSet = false + ) + ) + }, + onClick = { + viewModel.changeBearerToken(viewModel.debugSettingsData.activeProfileId) + } + ) + Button( + onClick = { scope.launch { viewModel.breakSSOToken() } }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Break SSO Token") + } + } + } + item { + DebugCard(title = "Environment") { + OutlinedDebugButton( + modifier = Modifier.fillMaxWidth(), + text = "Select Environment", + onClick = { scope.launch { modal.show() } } + ) + } + } + item { + VirtualHealthCard(viewModel = viewModel) + } + item { + FeatureToggles(viewModel = viewModel) + } + item { + RotatingLog(viewModel = viewModel) + } + } + } + } +} + +@Composable +private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { + DebugCard(modifier, title = "Log") { + val context = LocalContext.current + val mailAddress = stringResource(R.string.settings_contact_mail_address) + LabelButton( + modifier = Modifier.fillMaxWidth(), + icon = painterResource(R.drawable.ic_log_mail), + text = "Send mail", + onClick = { + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:") + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailAddress)) + intent.putExtra(Intent.EXTRA_SUBJECT, "#Log-#Android-${LocalDateTime.now()}") + + val bout = ByteArrayOutputStream() + ZipOutputStream(bout).use { + val e = ZipEntry("log.txt") + it.putNextEntry(e) + + val data = viewModel.rotatingLog.value.joinToString("\n").toByteArray() + it.write(data, 0, data.size) + it.closeEntry() + } + + intent.putExtra(Intent.EXTRA_TEXT, Base64.toBase64String(bout.toByteArray())) + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } + } + ) + } +} + +@Composable +private fun VirtualHealthCard(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { + var virtualHealthCardLoading by remember { mutableStateOf(false) } + var virtualHealthCardError by remember { mutableStateOf(null) } + virtualHealthCardError?.let { error -> + AlertDialog( + onDismissRequest = { + virtualHealthCardError = null + }, + buttons = { + Button(onClick = { virtualHealthCardError = null }) { + Text("OK") + } + }, + text = { + Text(error) + } + ) + } + + DebugCard(modifier, title = "Virtual Health Card", onReset = viewModel::onResetVirtualHealthCard) { + val scope = rememberCoroutineScope() + + OutlinedTextField( + modifier = Modifier + .testTag(TestTag.DebugMenu.CertificateField) + .heightIn(max = 144.dp) + .fillMaxWidth(), + value = viewModel.debugSettingsData.virtualHealthCardCert, + onValueChange = { + viewModel.onSetVirtualHealthCardCertificate(it) + }, + label = { Text("Certificate in Base64") } + ) + + val subjectInfo = + remember(viewModel.debugSettingsData.virtualHealthCardCert) { + viewModel.getVirtualHealthCardCertificateSubjectInfo() + } + Text(subjectInfo, style = AppTheme.typography.caption1l) + + OutlinedTextField( + modifier = Modifier + .testTag(TestTag.DebugMenu.PrivateKeyField) + .heightIn(max = 144.dp) + .fillMaxWidth(), + value = viewModel.debugSettingsData.virtualHealthCardPrivateKey, + onValueChange = { + viewModel.onSetVirtualHealthCardPrivateKey(it) + }, + label = { Text("Private Key in Base64") } + ) + + Button( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.DebugMenu.SetVirtualHealthCardButton), + onClick = { + virtualHealthCardLoading = true + scope.launch { + try { + viewModel.onTriggerVirtualHealthCard( + certificateBase64 = viewModel.debugSettingsData.virtualHealthCardCert, + privateKeyBase64 = viewModel.debugSettingsData.virtualHealthCardPrivateKey + ) + } catch (e: Exception) { + virtualHealthCardError = e.message + } finally { + virtualHealthCardLoading = false + } + } + }, + enabled = !virtualHealthCardLoading + ) { + if (virtualHealthCardLoading) { + CircularProgressIndicator( + Modifier.size(24.dp), + strokeWidth = 2.dp, + color = AppTheme.colors.neutral600 + ) + SpacerSmall() + } + Text("Set Virtual Health Card for Active Profile", textAlign = TextAlign.Center) + } + } +} + +@Composable +private fun FeatureToggles(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { + val featuresState by produceState(initialValue = mutableMapOf()) { + viewModel.featuresState().collect { + value = it + } + } + DebugCard(modifier, title = "Feature Toggles") { + for (feature in viewModel.features()) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = feature.featureName, + modifier = Modifier + .weight(1f), + style = MaterialTheme.typography.body1 + ) + Switch( + checked = featuresState[feature.featureName] ?: false, + onCheckedChange = { viewModel.toggleFeature(feature) } + ) + } + } + } +} + +@Composable +fun EnvironmentSelector( + currentSelectedEnvironment: Environment, + onSelectEnvironment: (environment: Environment) -> Unit, + onSaveEnvironment: () -> Unit +) { + var selectedEnvironment by remember { mutableStateOf(currentSelectedEnvironment) } + + Column( + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .selectableGroup() + ) { + Text( + text = stringResource(R.string.debug_select_environment), + style = AppTheme.typography.h6, + modifier = Modifier.padding(PaddingDefaults.Medium) + ) + + Environment.values().forEach { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + selectedEnvironment = it + onSelectEnvironment(it) + } + ) { + Row( + modifier = Modifier.padding( + horizontal = PaddingDefaults.Medium, + vertical = PaddingDefaults.Small + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + modifier = Modifier.size(32.dp), + selected = selectedEnvironment == it, + onClick = { + selectedEnvironment = it + onSelectEnvironment(it) + } + ) + Text(it.name) + } + } + } + Row(modifier = Modifier.padding(PaddingDefaults.Medium)) { + Button(modifier = Modifier.fillMaxWidth(), onClick = { onSaveEnvironment() }) { + Text(text = stringResource(R.string.debug_save_environment)) + } + Spacer(modifier = Modifier.navigationBarsPadding()) + } + } +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt new file mode 100644 index 00000000..76c0e894 --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController + +@Composable +fun DebugScreenWrapper(navigation: NavController) = + DebugScreen(settingsNavController = navigation) diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt new file mode 100644 index 00000000..40be1a3e --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/ui/DebugSettingsViewModel.kt @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.ui + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import de.gematik.ti.erp.app.BCProvider +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.ErezeptApp +import de.gematik.ti.erp.app.VisibleDebugTree +import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase +import de.gematik.ti.erp.app.data.DebugSettingsData +import de.gematik.ti.erp.app.data.Environment +import de.gematik.ti.erp.app.di.EndpointHelper +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.featuretoggle.Features +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.idp.usecase.IdpUseCase +import de.gematik.ti.erp.app.invoice.repository.InvoiceRepository +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase +import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase +import de.gematik.ti.erp.app.vau.repository.VauRepository +import io.github.aakira.napier.Napier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.util.encoders.Base64 +import org.bouncycastle.util.io.pem.PemReader +import org.jose4j.base64url.Base64Url +import org.jose4j.jws.EcdsaUsingShaAlgorithm +import java.math.BigInteger +import java.security.KeyFactory +import java.security.Signature +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* + +private val HealthCardCert = BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE +private val HealthCardCertPrivateKey = BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY + +@Suppress("LongParameterList") +class DebugSettingsViewModel( + visibleDebugTree: VisibleDebugTree, + private val endpointHelper: EndpointHelper, + private val cardWallUseCase: CardWallUseCase, + private val prescriptionUseCase: PrescriptionUseCase, + private val vauRepository: VauRepository, + private val idpRepository: IdpRepository, + private val invoiceRepository: InvoiceRepository, + private val idpUseCase: IdpUseCase, + private val profilesUseCase: ProfilesUseCase, + private val featureToggleManager: FeatureToggleManager, + private val pharmacyDirectRedeemUseCase: PharmacyDirectRedeemUseCase, + private val dispatchers: DispatchProvider +) : ViewModel() { + + var debugSettingsData by mutableStateOf(createDebugSettingsData()) + + val rotatingLog = visibleDebugTree.rotatingLog + + private fun createDebugSettingsData() = DebugSettingsData( + eRezeptServiceURL = endpointHelper.eRezeptServiceUri, + eRezeptActive = endpointHelper.isUriOverridden(EndpointHelper.EndpointUri.BASE_SERVICE_URI), + idpUrl = endpointHelper.idpServiceUri, + idpActive = endpointHelper.isUriOverridden(EndpointHelper.EndpointUri.IDP_SERVICE_URI), + pharmacyServiceUrl = endpointHelper.pharmacySearchBaseUri, + pharmacyServiceActive = endpointHelper.isUriOverridden(EndpointHelper.EndpointUri.PHARMACY_SERVICE_URI), + bearerToken = "", + bearerTokenIsSet = true, + fakeNFCCapabilities = cardWallUseCase.deviceHasNFCAndAndroidMOrHigher, + cardAccessNumberIsSet = false, + multiProfile = false, + activeProfileId = "", + virtualHealthCardCert = HealthCardCert, + virtualHealthCardPrivateKey = HealthCardCertPrivateKey + ) + + suspend fun state() { + val it = profilesUseCase.activeProfileId().first() + updateState( + debugSettingsData.copy( + cardAccessNumberIsSet = ( + cardWallUseCase.authenticationData(it) + .first().singleSignOnTokenScope as? IdpData.TokenWithHealthCardScope + )?.cardAccessNumber?.isNotEmpty() + ?: false, + activeProfileId = it, + bearerToken = idpRepository.decryptedAccessToken(it).first() ?: "" + ) + ) + } + + fun updateState(debugSettingsData: DebugSettingsData) { + this.debugSettingsData = debugSettingsData + } + + fun selectEnvironment(environment: Environment) { + updateState(getDebugSettingsdataForEnvironment(environment)) + } + + private fun getDebugSettingsdataForEnvironment(environment: Environment): DebugSettingsData { + return when (environment) { + Environment.PU -> debugSettingsData.copy( + eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_PU, + eRezeptActive = true, + idpUrl = BuildKonfig.IDP_SERVICE_URI_PU, + idpActive = true, + pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_PU, + pharmacyServiceActive = true + ) + + Environment.TU -> debugSettingsData.copy( + eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_TU, + eRezeptActive = true, + idpUrl = BuildKonfig.IDP_SERVICE_URI_TU, + idpActive = true, + pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_RU, + pharmacyServiceActive = true + ) + + Environment.RU -> debugSettingsData.copy( + eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_RU, + eRezeptActive = true, + idpUrl = BuildKonfig.IDP_SERVICE_URI_RU, + idpActive = true, + pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_RU, + pharmacyServiceActive = true + ) + + Environment.RUDEV -> debugSettingsData.copy( + eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_RU_DEV, + eRezeptActive = true, + idpUrl = BuildKonfig.IDP_SERVICE_URI_RU_DEV, + idpActive = true, + pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_RU, + pharmacyServiceActive = true + ) + + Environment.TR -> debugSettingsData.copy( + eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_TR, + eRezeptActive = true, + idpUrl = BuildKonfig.IDP_SERVICE_URI_TR, + idpActive = true, + pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_RU, + pharmacyServiceActive = true + ) + } + } + + fun changeBearerToken(activeProfileId: ProfileIdentifier) { + idpRepository.saveDecryptedAccessToken(activeProfileId, debugSettingsData.bearerToken) + updateState(debugSettingsData.copy(bearerTokenIsSet = true)) + } + + @RequiresApi(Build.VERSION_CODES.O) + suspend fun breakSSOToken() { + withContext(dispatchers.io) { + val activeProfileId = profilesUseCase.activeProfileId().first() + idpRepository.authenticationData(activeProfileId).first().singleSignOnTokenScope?.let { + val newToken = when (it) { + is IdpData.AlternateAuthenticationToken -> + IdpData.AlternateAuthenticationToken( + token = it.token?.breakToken(), + cardAccessNumber = it.cardAccessNumber, + aliasOfSecureElementEntry = it.aliasOfSecureElementEntry, + healthCardCertificate = it.healthCardCertificate.encoded + ) + + is IdpData.DefaultToken -> + IdpData.DefaultToken( + token = it.token?.breakToken(), + cardAccessNumber = it.cardAccessNumber, + healthCardCertificate = it.healthCardCertificate.encoded + ) + + is IdpData.ExternalAuthenticationToken -> + IdpData.ExternalAuthenticationToken( + token = it.token?.breakToken(), + authenticatorName = it.authenticatorName, + authenticatorId = it.authenticatorId + ) + + else -> it + } + idpRepository.saveSingleSignOnToken( + activeProfileId, + newToken + ) + Napier.d("SSO token is now: $newToken", tag = "Debug Settings") + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun IdpData.SingleSignOnToken.breakToken(): IdpData.SingleSignOnToken { + val (_, rest) = this.token.split('.', limit = 2) + val someHoursBeforeNow = Instant.now().minus(48, ChronoUnit.HOURS).epochSecond + val headerWithExpiresOn = Base64Url.encodeUtf8ByteRepresentation("""{"exp":$someHoursBeforeNow}""") + return IdpData.SingleSignOnToken("$headerWithExpiresOn.$rest") + } + + suspend fun saveAndRestartApp() { + endpointHelper.setUriOverride( + EndpointHelper.EndpointUri.BASE_SERVICE_URI, + debugSettingsData.eRezeptServiceURL, + debugSettingsData.eRezeptActive + ) + endpointHelper.setUriOverride( + EndpointHelper.EndpointUri.IDP_SERVICE_URI, + debugSettingsData.idpUrl, + debugSettingsData.idpActive + ) + endpointHelper.setUriOverride( + EndpointHelper.EndpointUri.PHARMACY_SERVICE_URI, + debugSettingsData.pharmacyServiceUrl, + debugSettingsData.pharmacyServiceActive + ) + profilesUseCase.profiles.flowOn(Dispatchers.IO).first().forEach { + idpRepository.invalidate(it.id) + } + vauRepository.invalidate() + restart() + } + + fun getCurrentEnvironment() = endpointHelper.getCurrentEnvironment() + + fun allowNfc(value: Boolean) { + cardWallUseCase.deviceHasNFCAndAndroidMOrHigher = value + updateState(debugSettingsData.copy(fakeNFCCapabilities = value)) + } + + fun refreshPrescriptions() { + viewModelScope.launch { + prescriptionUseCase.downloadTasks(profilesUseCase.activeProfileId().first()) + } + } + + fun features() = featureToggleManager.features + + fun featuresState() = + featureToggleManager.featuresState() + + fun toggleFeature(feature: Features) { + viewModelScope.launch { + val key = booleanPreferencesKey(feature.featureName) + featureToggleManager.toggleFeature(key) + } + } + + private fun restart() { + val context = ErezeptApp.applicationModule.androidContext() + val packageManager: PackageManager = context.packageManager + val intent = packageManager.getLaunchIntentForPackage(context.packageName) + val componentName = intent!!.component + val mainIntent = Intent.makeRestartActivityTask(componentName) + context.startActivity(mainIntent) + Runtime.getRuntime().exit(0) + } + + fun onResetVirtualHealthCard() { + updateState( + debugSettingsData.copy( + virtualHealthCardCert = HealthCardCert, + virtualHealthCardPrivateKey = HealthCardCertPrivateKey + ) + ) + } + + fun onSetVirtualHealthCardCertificate(cert: String) { + updateState(debugSettingsData.copy(virtualHealthCardCert = cert)) + } + + fun onSetVirtualHealthCardPrivateKey(privateKey: String) { + updateState(debugSettingsData.copy(virtualHealthCardPrivateKey = privateKey)) + } + + fun getVirtualHealthCardCertificateSubjectInfo(): String = + try { + X509CertificateHolder(Base64.decode(debugSettingsData.virtualHealthCardCert)).subject.toString() + } catch (e: Exception) { + e.message ?: "Error" + } + + suspend fun onTriggerVirtualHealthCard( + certificateBase64: String, + privateKeyBase64: String + ) = withContext(dispatchers.io) { + idpUseCase.authenticationFlowWithHealthCard( + profileId = profilesUseCase.activeProfileId().first(), + cardAccessNumber = "123123", + healthCardCertificate = { Base64.decode(certificateBase64) }, + sign = { + val curveSpec = ECNamedCurveTable.getParameterSpec("brainpoolP256r1") + val keySpec = + org.bouncycastle.jce.spec.ECPrivateKeySpec(BigInteger(Base64.decode(privateKeyBase64)), curveSpec) + val privateKey = KeyFactory.getInstance("EC", BCProvider).generatePrivate(keySpec) + val signed = Signature.getInstance("NoneWithECDSA").apply { + initSign(privateKey) + update(it) + }.sign() + EcdsaUsingShaAlgorithm.convertDerToConcatenated(signed, 64) + } + ) + } + + suspend fun redeemDirect( + url: String, + message: String, + certificatesPEM: String + ) { + val pemReader = PemReader(certificatesPEM.reader()) + + val certificates = mutableListOf() + do { + val obj = pemReader.readPemObject() + if (obj != null) { + certificates += X509CertificateHolder(obj.content) + } + } while (obj != null) + + pharmacyDirectRedeemUseCase.redeemPrescriptionDirectly( + url = url, + message = message, + telematikId = "", + recipientCertificates = certificates, + transactionId = UUID.randomUUID().toString() + ).getOrThrow() + } + + fun saveInvoice(invoiceBundle: String) { + viewModelScope.launch { + val profileId = profilesUseCase.activeProfileId().first() + val bundle = Json.parseToJsonElement(invoiceBundle) + invoiceRepository.saveInvoice(profileId, bundle) + } + } +} diff --git a/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt new file mode 100644 index 00000000..f358586a --- /dev/null +++ b/app/features/src/minifiedDebug/kotlin/de.gematik.ti.erp.app/utils/compose/DebugCommon.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import java.util.UUID + +@Composable +fun OutlinedDebugButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + OutlinedButton( + onClick = onClick, + border = ButtonDefaults.outlinedBorder.copy(brush = SolidColor(AppTheme.DebugColor)), + colors = ButtonDefaults.textButtonColors(contentColor = AppTheme.DebugColor), + contentPadding = PaddingValues(horizontal = PaddingDefaults.Small, vertical = PaddingDefaults.Tiny), + modifier = modifier.testTag(TestTag.Onboarding.SkipOnboardingButton) + ) { + Icon(Icons.Outlined.BugReport, null) + SpacerSmall() + Text(text) + SpacerTiny() + } +} + +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.visualTestTag(tag: String) = + composed(fullyQualifiedName = "de.gematik.ti.erp.app.utils.compose.visualTestTag", key1 = tag) { + val activity = LocalContext.current as MainActivity + val uuid = remember { UUID.randomUUID().toString() } + + DisposableEffect(tag) { + onDispose { + activity.elementsUsedInTests -= uuid + } + } + + Modifier + .testTag(tag) + .onGloballyPositioned { + activity.elementsUsedInTests += uuid to MainActivity.ElementForTest(it.boundsInRoot(), tag) + } + } + +@Composable +fun DebugOverlay(elements: Map) { + Box(Modifier.fillMaxSize()) { + elements.entries.forEach { (key, elementForTest) -> + key(key) { + Box( + Modifier + .layout { measurable, constraints -> + val placeable = measurable.measure( + Constraints.fixed( + elementForTest.bounds.width.toInt(), + elementForTest.bounds.height.toInt() + ) + ) + layout(placeable.width, placeable.height) { + placeable.place(elementForTest.bounds.topLeft.round()) + } + } + .border(width = 2.dp, color = Color.Magenta, shape = RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(12.dp)) + ) { + Text( + text = elementForTest.tag, + color = Color.Magenta, + overflow = TextOverflow.Visible, + modifier = Modifier + .background(Color.White.copy(alpha = 0.5f)) + .padding(start = 4.dp, end = 2.dp) + ) + } + } + } + } +} diff --git a/app/features/src/release/kotlin/de.gematik.ti.erp.app/debug/ui/DebugScreenWrapper.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt similarity index 95% rename from app/features/src/release/kotlin/de.gematik.ti.erp.app/debug/ui/DebugScreenWrapper.kt rename to app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt index 2b76a8f4..2572bc1e 100644 --- a/app/features/src/release/kotlin/de.gematik.ti.erp.app/debug/ui/DebugScreenWrapper.kt +++ b/app/features/src/release/kotlin/de.gematik.ti.erp.app/ui/DebugScreenWrapper.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.debug.ui +package de.gematik.ti.erp.app.ui import androidx.compose.runtime.Composable import androidx.navigation.NavController diff --git a/app/shared-test/build.gradle.kts b/app/shared-test/build.gradle.kts index bbb2da70..3c649085 100644 --- a/app/shared-test/build.gradle.kts +++ b/app/shared-test/build.gradle.kts @@ -18,10 +18,6 @@ plugins { id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") } -tasks.named("preBuild") { - dependsOn(":ktlint", ":detekt") -} - licenseReport { generateCsvReport = false generateHtmlReport = false @@ -45,6 +41,9 @@ android { it.name.startsWith("kapt") }.map { it.name } } + buildTypes { + create("minifiedDebug") + } } dependencies { diff --git a/assemble-mock.sh b/assemble-mock.sh new file mode 100755 index 00000000..632ef2f1 --- /dev/null +++ b/assemble-mock.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Get the last tag from Git +LAST_TAG=$(git tag --sort=-version:refname | head -n 1) + +# Get the last hash from Git +LAST_HASH=$(git rev-parse HEAD) + +# example version name R1.17.2-RC1-2e984eg356g +NEW_VERSION_NAME="$LAST_TAG-$LAST_HASH" + +# Get the current branch we are on +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Set the version code dynamically +# shellcheck disable=SC2003 +NEW_VERSION_CODE=$(expr "$(git rev-list --count "$CURRENT_BRANCH")") + +# Assemble the mock version +# shellcheck disable=SC2086 +./gradlew :app:android-mock:assembleDebug -PVERSION_CODE=$NEW_VERSION_CODE -PVERSION_NAME="$NEW_VERSION_NAME" diff --git a/build.gradle.kts b/build.gradle.kts index 675954b3..ecae840e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ plugins { id("io.realm.kotlin") version "1.7.1" apply false - // TODO: Update to latest version : https://github.com/JetBrains/compose-multiplatform/blob/master/VERSIONING.md + // TODO: Update to latest version : https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#kotlin-compatibility id("org.jetbrains.kotlin.android") version "1.9.10" apply false id("org.jetbrains.compose") version "1.5.3" apply false @@ -35,8 +35,8 @@ plugins { id("io.gitlab.arturbosch.detekt") version "1.22.0" id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") - id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false + id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false } val ktlintMain: Configuration by configurations.creating @@ -54,21 +54,20 @@ dependencies { val sourcesKt = listOf( "app/android/src/**/de/gematik/**/*.kt", "app/features/src/**/de/gematik/**/*.kt", + "app/demo-mode/src/**/de/gematik/**/*.kt", "common/src/**/de/gematik/**/*.kt", "desktop/src/**/de/gematik/**/*.kt", "rules/src/**/de/gematik/**/*.kt", "smartcard-wrapper/src/**/de/gematik/**/*.kt", - "plugins/*/src/**/*.kt", - - "**/*.gradle.kts" + "plugins/*/src/**/*.kt" ) detekt { + autoCorrect = false source = fileTree(rootDir) { include(sourcesKt) - } - .filter { it.extension != "kts" } + }.filter { it.extension != "kts" } .map { it.parentFile } .let { files(*it.toTypedArray()) @@ -107,9 +106,9 @@ tasks.register("clean", Delete::class) { } fun isUnstable(version: String): Boolean = - version.contains("alpha", ignoreCase = true) - || version.contains("rc", ignoreCase = true) - || version.contains("beta", ignoreCase = true) + version.contains("alpha", ignoreCase = true) || + version.contains("rc", ignoreCase = true) || + version.contains("beta", ignoreCase = true) tasks.withType { outputFormatter = "txt,html" diff --git a/common/build.gradle.kts b/common/build.gradle.kts index c5ad647a..55941d2e 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -206,6 +206,14 @@ android { sourceCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION targetCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION } + buildTypes { + val debug by getting { + isJniDebuggable = true + } + create("minifiedDebug") { + initWith(debug) + } + } namespace = "de.gematik.ti.erp.lib" } enum class Platforms { 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 2c3463ef..f3886795 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 @@ -55,7 +55,7 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmListOf import kotlinx.datetime.Instant -const val ACTUAL_SCHEMA_VERSION = 27L +const val ACTUAL_SCHEMA_VERSION = 28L val appSchemas = setOf( AppRealmSchema( 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 56f7fb8a..04397d4d 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 @@ -88,6 +88,9 @@ class SettingsEntityV1 : RealmObject, Cascading { var pharmacySearch: PharmacySearchEntityV1? = PharmacySearchEntityV1() var userHasAcceptedInsecureDevice: Boolean = false + + var userHasAcceptedIntegrityNotOk: Boolean = false + var dataProtectionVersionAccepted: RealmInstant = LocalDateTime(2021, 10, 15, 0, 0).toRealmInstant() var password: PasswordEntityV1? = PasswordEntityV1() @@ -104,6 +107,8 @@ class SettingsEntityV1 : RealmObject, Cascading { // `gemSpec_eRp_FdV A_20203` default settings are not allow screenshots var screenshotsAllowed: Boolean = false + var trackingAllowed: Boolean = false + override fun objectsToFollow(): Iterator = iterator { pharmacySearch?.let { yield(it) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt index d09b9abe..0c17ef78 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt @@ -67,6 +67,18 @@ object PharmacyUseCaseData { val isOnlineService get() = provides.any { it is OnlinePharmacyService } + val directRedeemUrlsNotPresent: Boolean + get() { + val hasNoPickupContact = contacts.pickUpUrl.isEmpty() + val hasNoDeliveryContact = contacts.deliveryUrl.isEmpty() + val hasNoOnlineServiceContact = contacts.onlineServiceUrl.isEmpty() + return listOf( + hasNoPickupContact, + hasNoDeliveryContact, + hasNoOnlineServiceContact + ).all { it } + } + @Stable fun singleLineAddress(): String = if (address.isNullOrEmpty()) { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt index 6cc156b0..e9acf5e5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt @@ -32,6 +32,7 @@ import de.gematik.ti.erp.app.profiles.model.ProfilesData import io.realm.kotlin.Realm import io.realm.kotlin.ext.query import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex @@ -280,4 +281,14 @@ class DefaultProfilesRepository constructor( } } } + + override suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean = + getProfileById(profileId).first().insuranceType == InsuranceTypeV1.PKV + + private fun getProfileById(profileId: ProfileIdentifier): Flow = + profiles().mapNotNull { profiles -> + profiles.find { + it.id == profileId + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt index cbe25dcc..c21c65e3 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt @@ -41,4 +41,5 @@ interface ProfileRepository { suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) suspend fun switchProfileToPKV(profileId: ProfileIdentifier) + suspend fun checkIsProfilePKV(profileId: ProfileIdentifier): Boolean } 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 1117f804..7ce37162 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 @@ -46,4 +46,6 @@ interface GeneralSettings { suspend fun saveMainScreenTooltipShown() suspend fun acceptMlKit() suspend fun saveAllowScreenshots(allow: Boolean) + suspend fun saveAllowTracking(allow: Boolean) + suspend fun acceptIntegrityNotOk() } 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 e55e408b..f5a7e2a5 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 @@ -30,8 +30,10 @@ object SettingsData { val mainScreenTooltipsShown: Boolean, val zoomEnabled: Boolean, val userHasAcceptedInsecureDevice: Boolean, + val userHasAcceptedIntegrityNotOk: Boolean, val authenticationFails: Int, val mlKitAccepted: Boolean, + val trackingAllowed: Boolean, val screenShotsAllowed: Boolean ) @@ -52,7 +54,7 @@ object SettingsData { } sealed class AuthenticationMode { - object DeviceSecurity : AuthenticationMode() + data object DeviceSecurity : AuthenticationMode() class Password : AuthenticationMode { val hash: ByteArray val salt: ByteArray @@ -90,6 +92,6 @@ object SettingsData { } } - object Unspecified : AuthenticationMode() + data object Unspecified : AuthenticationMode() } } 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 bb785863..26bcd921 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 @@ -66,11 +66,21 @@ class SettingsRepository constructor( authenticationFails = it.authenticationFails, mainScreenTooltipsShown = it.mainScreenTooltipsShown, mlKitAccepted = it.mlKitAccepted, - screenShotsAllowed = it.screenshotsAllowed + screenShotsAllowed = it.screenshotsAllowed, + trackingAllowed = it.trackingAllowed, + userHasAcceptedIntegrityNotOk = it.userHasAcceptedIntegrityNotOk ) } }.flowOn(dispatchers.io) + fun isAnalyticsAllowed() = settings.mapNotNull { settings -> settings?.trackingAllowed } + + suspend fun changeTrackingState(state: Boolean) { + writeToRealm { + this.trackingAllowed = state + } + } + // TODO: Not used override val authenticationMode: Flow get() = realm.query().first().asFlow().mapNotNull { query -> @@ -85,6 +95,7 @@ class SettingsRepository constructor( ) } } + else -> SettingsData.AuthenticationMode.Unspecified } } @@ -202,12 +213,24 @@ class SettingsRepository constructor( } } + override suspend fun saveAllowTracking(allow: Boolean) { + writeToRealm { + this.trackingAllowed = allow + } + } + override suspend fun acceptInsecureDevice() { writeToRealm { this.userHasAcceptedInsecureDevice = true } } + override suspend fun acceptIntegrityNotOk() { + writeToRealm { + this.userHasAcceptedIntegrityNotOk = true + } + } + override suspend fun acceptUpdatedDataTerms(now: Instant) { writeToRealm { this.setAcceptedUpdatedDataTerms(now) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt new file mode 100644 index 00000000..ececa085 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AcceptMLKitUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class AcceptMLKitUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.acceptMlKit() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowAnalyticsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowAnalyticsUseCase.kt new file mode 100644 index 00000000..c39bfc4b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowAnalyticsUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class AllowAnalyticsUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(allow: Boolean) = settingsRepository.saveAllowTracking(allow) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt new file mode 100644 index 00000000..b7a6b578 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/AllowScreenshotsUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class AllowScreenshotsUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(allow: Boolean) = settingsRepository.saveAllowScreenshots(allow) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt new file mode 100644 index 00000000..daeade3f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetCanStartToolTipsUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetCanStartToolTipsUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke(): Flow = settingsRepository.general.map { + it.welcomeDrawerShown && it.onboardingShownIn != null && !it.mainScreenTooltipsShown + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt new file mode 100644 index 00000000..05ea0e2d --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetMLKitAcceptedUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetMLKitAcceptedUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke(): Flow = settingsRepository.general.map { + it.mlKitAccepted + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt new file mode 100644 index 00000000..ab6241e4 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetOnboardingSucceededUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import io.realm.kotlin.internal.platform.runBlocking +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +class GetOnboardingSucceededUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke(): Boolean = runBlocking { + settingsRepository.general.map { + it.onboardingShownIn != null + }.first() + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt new file mode 100644 index 00000000..33eebb35 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetScreenShotsAllowedUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetScreenShotsAllowedUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke(): Flow = settingsRepository.general.map { + it.screenShotsAllowed + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt new file mode 100644 index 00000000..16cdfd5f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/GetShowWelcomeDrawerUseCase.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetShowWelcomeDrawerUseCase( + private val settingsRepository: SettingsRepository +) { + operator fun invoke(): Flow = settingsRepository.general.map { + !it.welcomeDrawerShown + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveAuthenticationModeUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveAuthenticationModeUseCase.kt new file mode 100644 index 00000000..6302bad5 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveAuthenticationModeUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class SaveAuthenticationModeUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(allow: Boolean) = settingsRepository.saveAllowScreenshots(allow) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingSuccededUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingSuccededUseCase.kt new file mode 100644 index 00000000..8dfbedd4 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveOnboardingSuccededUseCase.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.repository.SettingsRepository +import kotlinx.datetime.Clock + +class SaveOnboardingSuccededUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(authenticationMode: SettingsData.AuthenticationMode, profileName: String) = + settingsRepository.saveOnboardingSucceededData( + authenticationMode = authenticationMode, + profileName = profileName, + now = Clock.System.now() + ) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt new file mode 100644 index 00000000..01ae1352 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SavePasswordUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class SavePasswordUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke(password: String) = settingsRepository.saveAuthenticationMode( + SettingsData.AuthenticationMode.Password(password = password) + ) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt new file mode 100644 index 00000000..d5ddfaef --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveToolTippsShownUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class SaveToolTippsShownUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.saveMainScreenTooltipShown() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt new file mode 100644 index 00000000..3b07f51b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/settings/usecase/SaveWelcomeDrawerShownUseCase.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.usecase + +import de.gematik.ti.erp.app.settings.repository.SettingsRepository + +class SaveWelcomeDrawerShownUseCase( + private val settingsRepository: SettingsRepository +) { + suspend operator fun invoke() = settingsRepository.saveWelcomeDrawerShown() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt new file mode 100644 index 00000000..13d4f606 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/timeouts/repository/TimeoutRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.timeouts.repository + +import kotlin.time.Duration + +interface TimeoutRepository { + fun setDefaultTimeouts() + fun areTimeoutsExisting(): Boolean + fun changeInactivityTimeout(duration: Duration) + fun changePauseTimeout(duration: Duration) + fun getInactivityTimeout(): Duration + fun getPauseTimeout(): Duration +} 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 a74d3a93..89ab681f 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 @@ -34,9 +34,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant +import org.junit.Before import org.junit.Rule -import kotlin.test.Test -import kotlin.test.BeforeTest +import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -47,9 +47,9 @@ class SettingsRepositoryTest : TestDB() { lateinit var realm: Realm - lateinit var repo: SettingsRepository + private lateinit var repo: SettingsRepository - @BeforeTest + @Before fun setUp() { realm = Realm.open( RealmConfiguration.Builder( diff --git a/gradle.properties b/gradle.properties index 5a2492c6..ca6efd0f 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.17.0 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.18.1 GMTIK/eRezeptApp # DATA_PROTECTION_LAST_UPDATED=2022-01-06 # diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt index da65d519..8e47b0bf 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt @@ -51,7 +51,7 @@ object Dependencies { private const val desugar_version = "2.0.3" const val desugaring = "com.android.tools:desugar_jdk_libs:$desugar_version" const val processPhoenix = "com.jakewharton:process-phoenix:2.1.2" // TODO: Not used - const val imageCropper = "com.github.CanHub:Android-Image-Cropper:4.3.2" // TODO: remove lib for cropping + const val imageCropper = "com.github.CanHub:Android-Image-Cropper:4.3.2" } object AndroidX { @@ -64,7 +64,7 @@ object Dependencies { private const val security_crypto_version = "1.1.0-alpha06" private const val lifecycle_version = "2.6.2" // needs compile version 34 to go to a higher version - private const val compose_navigation_version = "2.6.0" // needs compile version 34 for 2.7.3 + private const val compose_navigation_version = "2.7.0-beta01" private const val compose_activity_version = "1.7.2" private const val compose_paging_version = "3.2.1" private const val camerax_version = "1.3.0-beta01" // needs compile version 34 for 1.3.0-rc02 @@ -292,9 +292,11 @@ object Dependencies { } object Tracking { - private const val content_square_version = "4.21.0" + private const val content_square_version = "4.22.0" const val contentSquare = "com.contentsquare.android:library:$content_square_version" + const val contentSquareCompose = "com.contentsquare.android:compose:$content_square_version" + const val contentSquareErrorAnalysis = "com.contentsquare.android:error-analysis:$content_square_version" } object Test { diff --git a/settings.gradle.kts b/settings.gradle.kts index 15008c2e..56ebef59 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,7 @@ dependencyResolutionManagement { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") google() mavenCentral() - maven ("https://jitpack.io") + maven("https://jitpack.io") } } @@ -42,11 +42,11 @@ includeBuild("smartcard-wrapper") { } } -//includeBuild("modules/fhir-parser") { +// includeBuild("modules/fhir-parser") { // dependencySubstitution { // substitute(module("de.gematik.ti.erp.app:fhir-parser")).using(project(":")) // } -//} +// } include(":app:android") include(":app:android-mock")