diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 7dfe581d..eb3c8e2b 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,7 @@ +# Release 1.9.0 +- Small refactorings +- Bugfixes + # Release 1.8.0 - Switched to new analytics tool - Bugfixes diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 3dc35ca8..6bda11c2 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -185,10 +185,6 @@ android { pickFirsts += "win32-x86/attach_hotspot_windows.dll" } } - - composeOptions { - kotlinCompilerExtensionVersion = "1.3.0" - } } compose.android.useAndroidX = true @@ -201,6 +197,10 @@ dependencies { implementation(kotlin("reflect")) testImplementation(kotlin("test")) + implementation("com.tom-roush:pdfbox-android:2.0.27.0") { + exclude(group = "org.bouncycastle") + } + app { dataMatrix { implementation(mlkitBarcodeScanner) diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt index 3e145520..29d6c76a 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt @@ -43,5 +43,5 @@ data class DebugSettingsData( ) : Parcelable enum class Environment { - PU, TU, RU, TR + PU, TU, RU, RUDEV, TR } diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt new file mode 100644 index 00000000..649793fd --- /dev/null +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.debug.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +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.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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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 org.kodein.di.compose.rememberViewModel + +@Composable +fun DebugScreenPKV(onBack: () -> Unit) { + val viewModel by rememberViewModel() + val listState = rememberLazyListState() + val context = LocalContext.current + + AnimatedElevationScaffold( + navigationMode = NavigationBarMode.Back, + listState = listState, + topBarTitle = "Debug PKV", + onBack = onBack + ) { innerPadding -> + var invoiceBundle by remember { mutableStateOf("") } + var attachement 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 = "Invoice Bundle" + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = invoiceBundle, + label = { Text("Bundle") }, + onValueChange = { + invoiceBundle = it + }, + maxLines = 1 + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = attachement, + label = { Text("Attachement") }, + onValueChange = { + attachement = it + }, + maxLines = 1 + ) + DebugLoadingButton( + onClick = { viewModel.shareInvoicePDF(context, invoiceBundle, attachement) }, + text = "Share" + ) + } + } + } + } +} diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt index e03e94aa..b79f6edf 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt @@ -59,7 +59,6 @@ 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.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState @@ -69,12 +68,9 @@ 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.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController @@ -108,7 +104,7 @@ import java.util.zip.ZipOutputStream import kotlin.math.max @Composable -private fun DebugCard( +fun DebugCard( modifier: Modifier = Modifier, title: String, onReset: (() -> Unit)? = null, @@ -283,6 +279,9 @@ fun DebugScreen( }, onClickDirectRedemption = { navController.navigate(DebugScreenNavigation.DebugRedeemWithoutFD.path()) + }, + onClickPKV = { + navController.navigate(DebugScreenNavigation.DebugPKV.path()) } ) } @@ -296,6 +295,15 @@ fun DebugScreen( ) } } + composable(DebugScreenNavigation.DebugPKV.route) { + NavigationAnimation(mode = navMode) { + DebugScreenPKV( + onBack = { + navController.popBackStack() + } + ) + } + } } } } @@ -435,7 +443,8 @@ private fun RedeemButton( @Composable fun DebugScreenMain( onBack: () -> Unit, - onClickDirectRedemption: () -> Unit + onClickDirectRedemption: () -> Unit, + onClickPKV: () -> Unit ) { val viewModel by rememberViewModel() val listState = rememberLazyListState() @@ -492,6 +501,12 @@ fun DebugScreenMain( ) { Text(text = "Direct Redemption") } + Button( + onClick = onClickPKV, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "PKV") + } Button( onClick = { viewModel.refreshPrescriptions() }, modifier = Modifier.fillMaxWidth() @@ -587,30 +602,14 @@ fun DebugScreenMain( } } -private const val maxNumberOfVisualLogs = 25 - @Composable private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsViewModel) { DebugCard(modifier, title = "Log") { - val logs by viewModel.rotatingLog.collectAsState(emptyList()) - val joinedLog = - logs.subList(max(0, logs.size - maxNumberOfVisualLogs), logs.size).fold(AnnotatedString("")) { acc, log -> - acc + AnnotatedString("\n") + log - } - - var text by remember(joinedLog) { mutableStateOf(TextFieldValue(joinedLog)) } - - Row { - val clipboard = LocalClipboardManager.current - Button(onClick = { clipboard.setText(joinedLog) }) { - Text("Copy All") - } - - Spacer(Modifier.weight(1f)) - - val context = LocalContext.current - val mailAddress = stringResource(R.string.settings_contact_mail_address) - Button(onClick = { + val context = LocalContext.current + val mailAddress = stringResource(R.string.settings_contact_mail_address) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { val intent = Intent(Intent.ACTION_SENDTO) intent.data = Uri.parse("mailto:") intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailAddress)) @@ -621,7 +620,7 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV val e = ZipEntry("log.txt") it.putNextEntry(e) - val data = joinedLog.text.toByteArray() + val data = viewModel.rotatingLog.value.joinToString("\n").toByteArray() it.write(data, 0, data.size) it.closeEntry() } @@ -631,21 +630,10 @@ private fun RotatingLog(modifier: Modifier = Modifier, viewModel: DebugSettingsV if (intent.resolveActivity(context.packageManager) != null) { context.startActivity(intent) } - }) { - Text("Send Mail") } + ) { + Text("Send Mail") } - - OutlinedTextField( - modifier = Modifier - .heightIn(max = 400.dp) - .fillMaxWidth(), - value = text, - readOnly = true, - onValueChange = { - text = it - } - ) } } diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt index ac9517e3..adac553d 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt @@ -23,4 +23,5 @@ import de.gematik.ti.erp.app.Route object DebugScreenNavigation { object DebugMain : Route("DebugMain") object DebugRedeemWithoutFD : Route("DebugRedeemWithoutFD") + object DebugPKV : Route("DebugPKV") } diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt index 4b1ad0fe..0c4dd548 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.debug.ui +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import androidx.compose.runtime.getValue @@ -37,9 +38,16 @@ import de.gematik.ti.erp.app.debug.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.fhir.model.extractPKVInvoiceBundle 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.usecase.PkvHtmlTemplate +import de.gematik.ti.erp.app.invoice.usecase.createPkvHtmlInvoiceTemplate +import de.gematik.ti.erp.app.invoice.usecase.createSharableFileInCache +import de.gematik.ti.erp.app.invoice.usecase.sharePDFFile +import de.gematik.ti.erp.app.invoice.usecase.writePDFAttachment +import de.gematik.ti.erp.app.invoice.usecase.writePdfFromHtml 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 @@ -51,6 +59,7 @@ 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 @@ -135,6 +144,7 @@ class DebugSettingsViewModel( pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_PU, pharmacyServiceActive = true ) + Environment.TU -> debugSettingsData.copy( eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_TU, eRezeptActive = true, @@ -143,6 +153,7 @@ class DebugSettingsViewModel( pharmacyServiceUrl = BuildKonfig.PHARMACY_SERVICE_URI_RU, pharmacyServiceActive = true ) + Environment.RU -> debugSettingsData.copy( eRezeptServiceURL = BuildKonfig.BASE_SERVICE_URI_RU, eRezeptActive = true, @@ -151,6 +162,14 @@ class DebugSettingsViewModel( 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, @@ -179,18 +198,21 @@ class DebugSettingsViewModel( 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( @@ -245,6 +267,55 @@ class DebugSettingsViewModel( } } + fun shareInvoicePDF(context: Context, bundle: String, attachement: String) { + viewModelScope.launch { + val html = extractPKVInvoiceBundle( + Json.parseToJsonElement(bundle), + processDispense = { whenHandedOver -> + whenHandedOver.formattedString() + }, + processPharmacyAddress = { line, postalCode, city -> + requireNotNull(line) + requireNotNull(postalCode) + requireNotNull(city) + (line + "$postalCode $city").joinToString() + }, + processPharmacy = { name, address, _, iknr, _, _ -> + PkvHtmlTemplate.createOrganization( + organizationName = requireNotNull(name), + organizationAddress = address, + organizationIKNR = iknr + ) + }, + processInvoice = { totalAdditionalFee, totalBruttoAmount, currency, items -> + PkvHtmlTemplate.createPriceData( + currency = currency, + totalBruttoAmount = totalBruttoAmount, + items = items + ) + }, + save = { pharmacy, invoice, dispense -> + createPkvHtmlInvoiceTemplate( + patient = "TODO", + patientBirthdate = "TODO", + prescriber = "TODO", + prescribedOn = "TODO", + organization = pharmacy, + dispensedOn = dispense, + priceData = invoice + ) + } + ) + + requireNotNull(html) { "HTML string required" } + + val file = createSharableFileInCache(context, "invoices", "invoice") + writePdfFromHtml(context, "Invoice", html, file) + writePDFAttachment(file, Triple("Test123", "text/plain", attachement.toByteArray())) + sharePDFFile(context, file) + } + } + fun features() = featureToggleManager.features fun featuresState() = diff --git a/android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt b/android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt index 063c3a46..d6a5edd6 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt +++ b/android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt @@ -92,6 +92,11 @@ class EndpointHelper( 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 -> { @@ -113,7 +118,7 @@ class EndpointHelper( when (getCurrentEnvironment()) { Environment.PU -> BuildKonfig.ERP_API_KEY_GOOGLE_PU Environment.TU -> BuildKonfig.ERP_API_KEY_GOOGLE_TU - Environment.RU -> BuildKonfig.ERP_API_KEY_GOOGLE_RU + Environment.RUDEV, Environment.RU -> BuildKonfig.ERP_API_KEY_GOOGLE_RU Environment.TR -> BuildKonfig.ERP_API_KEY_GOOGLE_TR } } else { @@ -121,6 +126,17 @@ class EndpointHelper( } } + 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()) { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 119a8d00..02c602c2 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -47,9 +47,19 @@ android:authorities="${applicationId}.mlkitinitprovider" tools:node="remove"/> + + + + + android:value="${MAPS_API_KEY}"/> - 2021-07-02 Datenschutzerklaerung-E-Rezept-App + Datenschutzerklaerung-E-Rezept-App @@ -12,7 +12,7 @@

Datenschutzerklärung

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

-

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

+

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

1. Über diese Datenschutzerklärung

@@ -165,7 +165,7 @@

9. Ansprechpartner

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

diff --git a/android/src/main/java/android/print/PdfPrinter.kt b/android/src/main/java/android/print/PdfPrinter.kt new file mode 100644 index 00000000..2925733b --- /dev/null +++ b/android/src/main/java/android/print/PdfPrinter.kt @@ -0,0 +1,28 @@ +package android.print + +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import java.io.File +import java.io.OutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class PdfPrint(private val printAttributes: PrintAttributes) { + suspend fun print(printAdapter: PrintDocumentAdapter, outputFd: File) = suspendCoroutine { continuation -> + printAdapter.onLayout(null, printAttributes, null, object : PrintDocumentAdapter.LayoutResultCallback() { + override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) { + printAdapter.onWrite( + arrayOf(PageRange.ALL_PAGES), + ParcelFileDescriptor.open(outputFd, ParcelFileDescriptor.MODE_READ_WRITE), + CancellationSignal(), + object : PrintDocumentAdapter.WriteResultCallback() { + override fun onWriteFinished(pages: Array) { + super.onWriteFinished(pages) + continuation.resumeWith(Result.success(Unit)) + } + } + ) + } + }, null) + } +} \ No newline at end of file diff --git a/android/src/main/java/de/gematik/ti/erp/app/App.kt b/android/src/main/java/de/gematik/ti/erp/app/App.kt index 36a97cb7..3b0b0346 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/App.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/App.kt @@ -22,6 +22,7 @@ import android.app.Application import android.content.Context import androidx.lifecycle.ProcessLifecycleOwner import com.contentsquare.android.Contentsquare +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader import de.gematik.ti.erp.app.core.AppScopedCache import de.gematik.ti.erp.app.di.allModules import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase @@ -58,6 +59,8 @@ class App : Application(), DIAware { addObserver(authUseCase) } + PDFBoxResourceLoader.init(this) + Contentsquare.start(this) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt b/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt index c27b6f6f..ca3c4485 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt @@ -61,29 +61,25 @@ import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalAuthPrompt import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPrompt import de.gematik.ti.erp.app.cardwall.mini.ui.MiniCardWallViewModel import de.gematik.ti.erp.app.cardwall.mini.ui.rememberAuthenticator -import de.gematik.ti.erp.app.cardwall.ui.CardWallNfcPositionViewModel import de.gematik.ti.erp.app.cardwall.ui.CardWallController import de.gematik.ti.erp.app.cardwall.ui.ExternalAuthenticatorListViewModel import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.core.MainContent -import de.gematik.ti.erp.app.core.MainViewModel import de.gematik.ti.erp.app.di.ApplicationPreferencesTag import de.gematik.ti.erp.app.mainscreen.ui.MainScreen -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel import de.gematik.ti.erp.app.orderhealthcard.ui.HealthCardOrderViewModel import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailsViewModel -import de.gematik.ti.erp.app.prescription.ui.PrescriptionViewModel import de.gematik.ti.erp.app.prescription.ui.ScanPrescriptionViewModel import de.gematik.ti.erp.app.profiles.ui.ProfileSettingsViewModel import de.gematik.ti.erp.app.profiles.ui.ProfileViewModel -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel import de.gematik.ti.erp.app.analytics.Analytics import de.gematik.ti.erp.app.apicheck.usecase.CheckVersionUseCase import de.gematik.ti.erp.app.cardwall.mini.ui.SecureHardwarePrompt import de.gematik.ti.erp.app.core.IntentHandler import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.mainscreen.ui.rememberMainScreenController import de.gematik.ti.erp.app.pharmacy.repository.model.PharmacyOverviewViewModel import de.gematik.ti.erp.app.prescription.detail.ui.SharePrescriptionHandler import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler @@ -121,12 +117,10 @@ class MainActivity : AppCompatActivity(), DIAware { bindProvider { UnlockEgkViewModel(instance(), instance()) } bindProvider { MiniCardWallViewModel(instance(), instance(), instance(), instance(), instance()) } - bindProvider { CardWallNfcPositionViewModel(instance()) } bindProvider { CardWallController(instance(), instance(), instance()) } bindProvider { ExternalAuthenticatorListViewModel(instance(), instance()) } bindProvider { HealthCardOrderViewModel(instance()) } bindProvider { PrescriptionDetailsViewModel(instance(), instance()) } - bindProvider { PrescriptionViewModel(instance(), instance(), instance()) } bindProvider { ScanPrescriptionViewModel( prescriptionUseCase = instance(), @@ -141,20 +135,6 @@ class MainActivity : AppCompatActivity(), DIAware { bindProvider { ProfileSettingsViewModel(instance(), instance()) } bindProvider { UserAuthenticationViewModel(instance()) } bindProvider { PharmacyOverviewViewModel(instance()) } - - bindSingleton { - SettingsViewModel( - settingsUseCase = instance(), - profilesUseCase = instance(), - profilesWithPairedDevicesUseCase = instance(), - analytics = instance(), - appPrefs = instance(ApplicationPreferencesTag), - dispatchers = instance() - ) - } - bindSingleton { MainViewModel(instance(), instance()) } - bindSingleton { MainScreenViewModel(instance()) } - bindProvider { CheckVersionUseCase(instance(), instance()) } if (BuildConfig.DEBUG && BuildKonfig.INTERNAL) { @@ -243,7 +223,7 @@ class MainActivity : AppCompatActivity(), DIAware { ) { val authenticator = LocalAuthenticator.current - MainContent { mainViewModel -> + MainContent { settingsController -> val auth by produceState(null) { launch { authenticationModeAndMethod.distinctUntilChangedBy { it::class } @@ -286,18 +266,16 @@ class MainActivity : AppCompatActivity(), DIAware { authenticator = authenticator.authenticatorSecureElement ) - val settingsViewModel by rememberViewModel() + val mainScreenController = rememberMainScreenController() val profileSettingsViewModel by rememberViewModel() - val mainScreenViewModel by rememberViewModel() CompositionLocalProvider( LocalProfileHandler provides rememberProfileHandler() ) { MainScreen( navController = navController, - mainViewModel = mainViewModel, - settingsViewModel = settingsViewModel, - mainScreenViewModel = mainScreenViewModel, + settingsController = settingsController, + mainScreenController = mainScreenController, profileSettingsViewModel = profileSettingsViewModel ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt b/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt index a7520ec2..71b59aed 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt @@ -362,6 +362,8 @@ object TestTag { object Profile { val ProfileScreen by tagName() val ProfileScreenContent by tagName() + val InvoicesScreen by tagName() + val InvoicesScreenContent by tagName() val OpenTokensScreenButton by tagName() val InsuranceId by tagName() val LoginButton by tagName() diff --git a/android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt b/android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt index f49c05aa..95b1460b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt @@ -18,42 +18,39 @@ package de.gematik.ti.erp.app -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle import io.github.aakira.napier.Antilog import io.github.aakira.napier.LogLevel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +private const val MaxLogEntries = 5000 +private const val LogEntriesToDelete = 100 + class VisibleDebugTree : Antilog() { - val rotatingLog = MutableStateFlow>(emptyList()) + private val _rotatingLog = MutableStateFlow>(emptyList()) + val rotatingLog: StateFlow> + get() = _rotatingLog override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) { - rotatingLog.update { - if (it.size > 500) { - it.drop(10) + _rotatingLog.update { + if (it.size > MaxLogEntries + LogEntriesToDelete) { + it.drop(LogEntriesToDelete) } else { it - } + buildAnnotatedString { - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(tag ?: "unknown") - } - append(" ") - if (priority == LogLevel.ERROR) { - withStyle(SpanStyle(color = Color.Red)) { - append(message ?: "") - } - } else { - append(message ?: "") + } + buildString { + val lvl = when (priority) { + LogLevel.VERBOSE -> "V" + LogLevel.DEBUG -> "D" + LogLevel.INFO -> "I" + LogLevel.WARNING -> "W" + LogLevel.ERROR -> "E" + LogLevel.ASSERT -> "A" } + append("${ tag ?: "unknown" } $lvl: ${message ?: ""}") throwable?.run { - withStyle(SpanStyle(color = Color.Red)) { - append(throwable.message ?: "") - } + append("\n") + append(throwable.stackTraceToString()) } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt b/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt index f2e8e998..a9847f36 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt @@ -68,7 +68,7 @@ class Analytics constructor( } } - fun tagScreen(screenName: String) { + fun trackScreen(screenName: String) { if (analyticsAllowed.value) { Contentsquare.send(screenName) Napier.d("Analytics send $screenName") @@ -100,28 +100,27 @@ class Analytics constructor( } fun trackIdentifiedWithIDP() { - // noop + trackScreen("idp_authenticated") } - enum class AuthenticationProblem { - CardBlocked, - CardAccessNumberWrong, - CardCommunicationInterrupted, - CardPinWrong, - IDPCommunicationFailed, - IDPCommunicationInvalidCertificate, - IDPCommunicationInvalidOCSPOfCard, - SecureElementCryptographyFailed, - UserNotAuthenticated + enum class AuthenticationProblem(val event: String) { + CardBlocked("card_blocked"), + CardAccessNumberWrong("card_can_wrong"), + CardCommunicationInterrupted("card_com_interrupted"), + CardPinWrong("card_pin_wrong"), + IDPCommunicationFailed("idp_com_failed"), + IDPCommunicationInvalidCertificate("idp_com_invalid_certificate"), + IDPCommunicationInvalidOCSPOfCard("idp_com_invalid_ocsp_of_card"), + SecureElementCryptographyFailed("secure_element_cryptography_failed"), + UserNotAuthenticated("user_not_authenticated") } - @Suppress("UnusedPrivateMember") fun trackAuthenticationProblem(kind: AuthenticationProblem) { - // noop + trackScreen("auth_error_${kind.event}") } fun trackSaveScannedPrescriptions() { - // noop + trackScreen("pres_scanned_saved") } } @@ -132,7 +131,7 @@ fun TrackNavigationChanges(navController: NavHostController) { LaunchedEffect(Unit) { navController.currentBackStackEntryFlow.collect { try { - analytics.tagScreen(Uri.parse(it.destination.route).buildUpon().clearQuery().build().toString()) + analytics.trackScreen(Uri.parse(it.destination.route).buildUpon().clearQuery().build().toString()) } catch (expected: Exception) { Napier.e("Couldn't track navigation screen", expected) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt index 80ade9b1..97036258 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt @@ -49,10 +49,10 @@ 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 import de.gematik.ti.erp.app.cardwall.ui.CardHandlingScaffold -import de.gematik.ti.erp.app.cardwall.ui.CardWallNfcPositionViewModel import de.gematik.ti.erp.app.cardwall.ui.ConformationSecretInputField import de.gematik.ti.erp.app.cardwall.ui.NFCInstructionScreen import de.gematik.ti.erp.app.cardwall.ui.SecretInputField +import de.gematik.ti.erp.app.cardwall.ui.rememberCardWallNfcPositionState import de.gematik.ti.erp.app.troubleShooting.TroubleShootingScreen import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme @@ -581,8 +581,8 @@ private fun UnlockScreen( onFinishUnlock: () -> Unit, onAssignPin: () -> Unit ) { - val nfcPositionViewModel by rememberViewModel() - val state by remember { mutableStateOf(nfcPositionViewModel.screenState()) } + val nfcPositionState = rememberCardWallNfcPositionState() + val state = nfcPositionState.state val dialogState = rememberUnlockEgkDialogState() UnlockEgkDialog( diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt index 72aff0bd..ccc796b1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt @@ -68,7 +68,6 @@ import com.airbnb.lottie.compose.rememberLottieComposition import com.google.accompanist.systemuicontroller.rememberSystemUiController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.cardwall.ui.model.CardWallNfcPositionViewModelData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -76,7 +75,6 @@ import de.gematik.ti.erp.app.utils.compose.NavigationBack import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.TopAppBar import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import org.kodein.di.compose.rememberViewModel import kotlin.math.PI import kotlin.math.cos @@ -122,8 +120,8 @@ fun CardWallNfcInstructionScreen( onRetryCan: () -> Unit, onRetryPin: () -> Unit ) { - val nfcPositionViewModel by rememberViewModel() - val state by remember { mutableStateOf(nfcPositionViewModel.screenState()) } + val nfcPositionState = rememberCardWallNfcPositionState() + val state = nfcPositionState.state val dialogState = rememberCardWallAuthenticationDialogState() @@ -150,11 +148,12 @@ fun CardWallNfcInstructionScreen( NFCInstructionScreen(onBack, onClickTroubleshooting, state) } +@Suppress("LongMethod") @Composable fun NFCInstructionScreen( onBack: () -> Unit, onClickTroubleshooting: () -> Unit, - state: CardWallNfcPositionViewModelData.NfcPosition + state: CardWallNfcPositionStateData.State ) { val useDarkIcons = MaterialTheme.colors.isLight AppTheme( @@ -182,8 +181,8 @@ fun NFCInstructionScreen( var titleHeight by remember { mutableStateOf(0) } var subTitleHeight by remember { mutableStateOf(0) } var descriptionHeight by remember { mutableStateOf(0) } - val nfcXPos by remember { mutableStateOf((state.nfcPosition.x0 + state.nfcPosition.x1) / 2) } - val nfcYPos by remember { mutableStateOf((state.nfcPosition.y0 + state.nfcPosition.y1) / 2) } + val nfcXPos by remember { mutableStateOf((state.nfcData.nfcPos.x0 + state.nfcData.nfcPos.x1) / 2) } + val nfcYPos by remember { mutableStateOf((state.nfcData.nfcPos.y0 + state.nfcData.nfcPos.y1) / 2) } LazyColumn( state = lazyListState, diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt new file mode 100644 index 00000000..1d0e8c75 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.cardwall.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.cardwall.usecase.CardWallLoadNfcPositionUseCase +import de.gematik.ti.erp.app.cardwall.usecase.model.NfcPositionUseCaseData +import org.kodein.di.compose.rememberInstance + +class CardWallNfcPositionState( + nfcPositionUseCase: CardWallLoadNfcPositionUseCase +) { + private val findNfc = nfcPositionUseCase.findNfcPositionForPhone() + + val state = findNfc?.let { + CardWallNfcPositionStateData.State(findNfc) + } ?: CardWallNfcPositionStateData.defaultState +} + +@Composable +fun rememberCardWallNfcPositionState(): CardWallNfcPositionState { + val nfcPositionUseCase by rememberInstance() + + return remember { + CardWallNfcPositionState(nfcPositionUseCase) + } +} + +object CardWallNfcPositionStateData { + @Immutable + data class State( + val nfcData: NfcPositionUseCaseData.NfcData + ) + + val defaultState = State( + NfcPositionUseCaseData.NfcData( + manufacturer = "", + marketingName = "", + modelNames = emptyList(), + nfcPos = NfcPositionUseCaseData.NfcPos( + x0 = 0.5, + y0 = 0.3, + x1 = 0.5, + y1 = 0.3 + ) + ) + ) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionViewModel.kt deleted file mode 100644 index c06f5343..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui - -import de.gematik.ti.erp.app.cardwall.ui.model.CardWallNfcPositionViewModelData -import de.gematik.ti.erp.app.cardwall.usecase.CardWallLoadNfcPositionUseCase -import androidx.lifecycle.ViewModel - -class CardWallNfcPositionViewModel( - private val nfcPositionUseCase: CardWallLoadNfcPositionUseCase -) : ViewModel() { - val defaultState = CardWallNfcPositionViewModelData.NfcPosition() - - private val findNfc = nfcPositionUseCase.findNfcPositionForPhone() - - fun screenState() = findNfc?.let { CardWallNfcPositionViewModelData.NfcPosition(findNfc) } ?: defaultState -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNfcPositionViewModelData.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNfcPositionViewModelData.kt deleted file mode 100644 index c6b24c28..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/CardWallNfcPositionViewModelData.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.ui.model - -import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.cardwall.usecase.model.NfcPositionUseCaseData - -object CardWallNfcPositionViewModelData { - @Immutable - data class NfcPosition( - val nfcPosition: NfcPositionUseCaseData.NfcPosition = - NfcPositionUseCaseData.NfcPosition( - marketingName = "", - modelNames = emptyList(), - x0 = 0.5, - y0 = 0.3, - x1 = 0.5, - y1 = 0.3 - ) - ) -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt index 5ce04172..6fb8ae61 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt @@ -29,7 +29,7 @@ import java.io.InputStream class CardWallLoadNfcPositionUseCase( private val context: Context ) { - private val nfcPositions: List by lazy { + private val nfcPositions: List by lazy { loadNfcPositionsFromJSON( context.resources.openRawResourceFd(R.raw.nfc_positions).createInputStream() ).sortedBy { it.marketingName.lowercase() } @@ -38,5 +38,5 @@ class CardWallLoadNfcPositionUseCase( fun findNfcPositionForPhone() = nfcPositions.find { it.modelNames.contains(Build.MODEL) } } -private fun loadNfcPositionsFromJSON(jsonInput: InputStream): List = +private fun loadNfcPositionsFromJSON(jsonInput: InputStream): List = Json.decodeFromString(jsonInput.bufferedReader().readText()) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt index 6aa7c980..dd63727e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt @@ -24,9 +24,16 @@ import kotlinx.serialization.Serializable object NfcPositionUseCaseData { @Immutable @Serializable - data class NfcPosition( + data class NfcData( + val manufacturer: String, val marketingName: String, val modelNames: List, + val nfcPos: NfcPos + ) + + @Immutable + @Serializable + data class NfcPos( val x0: Double, val y0: Double, val x1: Double, diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt b/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt index f2901067..75b03c21 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt @@ -32,7 +32,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -55,12 +54,13 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.settings.ui.SettingsController +import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberViewModel import kotlin.math.max import kotlin.math.min @@ -75,10 +75,10 @@ val LocalAnalytics = @Composable fun MainContent( - content: @Composable (mainViewModel: MainViewModel) -> Unit + content: @Composable (settingsController: SettingsController) -> Unit ) { - val mainViewModel by rememberViewModel() - val zoomEnabled by mainViewModel.zoomEnabled.collectAsState(false) + val settingsController = rememberSettingsController() + val zoomState by settingsController.zoomState AppTheme { val systemUiController = rememberSystemUiController() @@ -88,9 +88,9 @@ fun MainContent( } Box( - modifier = Modifier.zoomable(enabled = zoomEnabled) + modifier = Modifier.zoomable(enabled = zoomState.zoomEnabled) ) { - content(mainViewModel) + content(settingsController) } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt deleted file mode 100644 index 47fdf9d6..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/core/MainViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.core - -import android.accessibilityservice.AccessibilityServiceInfo -import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import de.gematik.ti.erp.app.attestation.usecase.IntegrityUseCase -import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking - -class MainViewModel( - private val integrityUseCase: IntegrityUseCase, - private val settingsUseCase: SettingsUseCase -) : ViewModel() { - val zoomEnabled = settingsUseCase.general.map { it.zoomEnabled } - val authenticationMethod = settingsUseCase.authenticationMode - 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) { - insecureDevicePromptShown = true - it - } else { - false - } - } - - var integrityPromptShown = false - - fun checkDeviceIntegrity() = integrityUseCase.runIntegrityAttestation().map { - if (!it && !integrityPromptShown) { - integrityPromptShown = true - false - } else { - true - } - } - - fun onAcceptInsecureDevice() { - viewModelScope.launch { - settingsUseCase.acceptInsecureDevice() - } - } - - fun acceptMlKit() { - viewModelScope.launch { - settingsUseCase.acceptMlKit() - } - } - - fun acceptUpdatedDataTerms() { - viewModelScope.launch { - settingsUseCase.acceptUpdatedDataTerms() - } - } - - 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() - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt b/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt index 7fdebc27..5fbfe37a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt @@ -31,6 +31,7 @@ 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.pharmacyModule +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 @@ -97,6 +98,7 @@ val allModules = DI.Module("allModules") { taskModule, settingsModule, vauModule, - cardUnlockModule + cardUnlockModule, + pkvModule ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt b/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt index c4cca19d..d0d5726c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.idp +import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.di.NetworkSecurePreferencesTag import de.gematik.ti.erp.app.idp.repository.IdpLocalDataSource import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository @@ -37,7 +38,10 @@ import org.kodein.di.instance val idpModule = DI.Module("idpModule") { bindProvider { IdpLocalDataSource(instance()) } bindProvider { IdpPairingRepository(instance()) } - bindProvider { IdpRemoteDataSource(instance()) } + bindProvider { + val endpointHelper = instance() + IdpRemoteDataSource(instance()) { endpointHelper.getIdpScope() } + } bindProvider { IdpAlternateAuthenticationUseCase(instance(), instance(), instance()) } bindProvider { IdpCryptoProvider() } bindProvider { IdpDeviceInfoProvider() } diff --git a/android/src/main/java/de/gematik/ti/erp/app/invoice/usecase/CreatePdf.kt b/android/src/main/java/de/gematik/ti/erp/app/invoice/usecase/CreatePdf.kt new file mode 100644 index 00000000..dfe793a8 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/invoice/usecase/CreatePdf.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import android.content.Context +import android.content.Intent +import android.print.PdfPrint +import android.print.PrintAttributes +import android.print.PrintDocumentAdapter +import android.util.Base64 +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.content.FileProvider +import com.tom_roush.pdfbox.pdmodel.PDDocument +import com.tom_roush.pdfbox.pdmodel.PDDocumentNameDictionary +import com.tom_roush.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode +import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification +import com.tom_roush.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile +import io.github.aakira.napier.Napier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.util.GregorianCalendar +import java.util.UUID +import kotlin.coroutines.suspendCoroutine + +private const val FileProviderAuthority = "de.gematik.ti.erp.app.fileprovider" +private const val PDFDensity = 600 +private const val PDFMargin = 24 + +fun createSharableFileInCache(context: Context, path: String, filePrefix: String): File { + val uuid = UUID.randomUUID().toString() + val pdfPath = File(context.cacheDir, path).apply { + mkdirs() + // clean up old codes + listFiles()?.forEach { + if (!it.isDirectory) { + Napier.d("Delete cache file ${it.name}") + it.delete() + } + } + } + val newFile = File(pdfPath, "$filePrefix-$uuid.pdf") + Napier.d("Created cache file ${newFile.name}") + + newFile.createNewFile() + + return newFile +} + +fun sharePDFFile(context: Context, file: File) { + val contentUri = FileProvider.getUriForFile(context, FileProviderAuthority, file) + + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, "") + putExtra(Intent.EXTRA_STREAM, contentUri) + type = "application/pdf" + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + context.startActivity(Intent.createChooser(shareIntent, null)) +} + +suspend fun writePdfFromHtml(context: Context, title: String, html: String, out: File) = + withContext(Dispatchers.Main) { + val webView: WebView + suspendCoroutine { continuation -> + webView = WebView(context).apply { + settings.javaScriptEnabled = false + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + continuation.resumeWith(Result.success(Unit)) + } + } + } + + webView.loadData( + Base64.encodeToString(html.encodeToByteArray(), Base64.NO_PADDING), + "text/html", + "base64" + ) + } + val adapter = webView.createPrintDocumentAdapter(title) + writePDF(adapter, out) + webView.destroy() + } + +suspend fun writePDF(adapter: PrintDocumentAdapter, out: File) { + val attributes = PrintAttributes.Builder() + .setMediaSize(PrintAttributes.MediaSize.ISO_A4) + .setResolution(PrintAttributes.Resolution("pdf", "pdf", PDFDensity, PDFDensity)) + .setMinMargins(PrintAttributes.Margins(PDFMargin, PDFMargin, PDFMargin, PDFMargin)).build() + val pdfPrint = PdfPrint(attributes) + + pdfPrint.print(adapter, out) +} + +fun writePDFAttachment(out: File, vararg attachments: Triple) { + val doc = PDDocument.load(out) + + val efTree = PDEmbeddedFilesNameTreeNode() + + efTree.names = attachments.associate { (name, type, data) -> + val ef = PDEmbeddedFile(doc, data.inputStream()) + ef.subtype = type + ef.size = data.size + ef.creationDate = GregorianCalendar() + + val fs = PDComplexFileSpecification() + fs.file = name + fs.embeddedFile = ef + + name to fs + } + + val names = PDDocumentNameDictionary(doc.documentCatalog) + names.embeddedFiles = efTree + doc.documentCatalog.names = names + + doc.save(out) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataProtectionDifferences.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataProtectionDifferences.kt deleted file mode 100644 index b607c2c9..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/DataProtectionDifferences.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.mainscreen.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowDropDown -import androidx.compose.material.icons.rounded.ArrowDropUp -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.Spacer32 -import de.gematik.ti.erp.app.utils.compose.Spacer8 -import de.gematik.ti.erp.app.utils.compose.annotatedLinkStringLight -import de.gematik.ti.erp.app.utils.compose.annotatedStringResource - -@Composable -fun DPDifferences30112021() { - DPSection(title = stringResource(R.string.data_terms_first_update_header)) { - Text( - stringResource(R.string.data_terms_first_update_text), - modifier = Modifier.fillMaxWidth(), - style = AppTheme.typography.body2l - ) - } - Spacer32() - DPSection(title = stringResource(R.string.data_terms_second_update_header)) { - val uriHandler = LocalUriHandler.current - - val policiesLink = annotatedLinkStringLight( - uri = stringResource(R.string.google_policies_link), - text = stringResource(R.string.google_policies_link) - ) - val supportLink = annotatedLinkStringLight( - uri = stringResource(R.string.google_support_link), - text = stringResource(R.string.google_support_link) - ) - - val text = annotatedStringResource( - R.string.data_terms_second_update_text, - policiesLink, - supportLink - ) - ClickableText( - text = text, - style = AppTheme.typography.body2l, - onClick = { - text - .getStringAnnotations("URL", it, it) - .firstOrNull()?.let { stringAnnotation -> - uriHandler.openUri(stringAnnotation.item) - } - }, - modifier = Modifier - .padding(end = PaddingDefaults.Medium) - ) - } -} - -@Composable -fun DPSection(title: String, content: @Composable () -> Unit) { - var sectionExpanded by remember { mutableStateOf(false) } - val arrow = if (sectionExpanded) { - Icons.Rounded.ArrowDropUp - } else { - Icons.Rounded.ArrowDropDown - } - - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = { sectionExpanded = !sectionExpanded }) - .padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.Tiny) - ) { - Text( - title, - modifier = Modifier.weight(1f), - style = AppTheme.typography.body1 - ) - Icon( - imageVector = arrow, - contentDescription = "", - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterVertically), - tint = AppTheme.colors.primary600 - ) - } - Spacer8() - AnimatedVisibility( - visible = sectionExpanded, - modifier = Modifier.padding( - start = PaddingDefaults.Medium, - top = PaddingDefaults.Tiny, - bottom = PaddingDefaults.Medium, - end = 48.dp - ) - ) { - content() - } - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt index 1eb6e8d0..3e424bbe 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt @@ -42,6 +42,7 @@ 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 @@ -56,7 +57,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavController import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.core.MainViewModel +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 @@ -64,12 +65,13 @@ 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 @Composable fun InsecureDeviceScreen( navController: NavController, - mainViewModel: MainViewModel, + settingsController: SettingsController, headline: String, icon: Painter, headlineBody: String, @@ -79,6 +81,7 @@ fun InsecureDeviceScreen( ) { var checked by rememberSaveable { mutableStateOf(false) } val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() AnimatedElevationScaffold( elevated = scrollState.value > 0, @@ -89,7 +92,9 @@ fun InsecureDeviceScreen( Button( onClick = { if (checked && pinUseCase) { - mainViewModel.onAcceptInsecureDevice() + scope.launch { + settingsController.onAcceptInsecureDevice() + } } navController.popBackStack() }, diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt index 3f08b132..bd11aeed 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.mainscreen.ui -import android.media.Image import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -39,7 +38,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable 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 @@ -50,7 +48,6 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role.Companion.Image import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -58,7 +55,6 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.core.MainViewModel import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.ui.AvatarPicker import de.gematik.ti.erp.app.profiles.ui.ColorPicker @@ -66,8 +62,7 @@ import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.profiles.ui.ProfileImage import de.gematik.ti.erp.app.profiles.ui.ProfileSettingsViewModel import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +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.PrimaryButton @@ -94,8 +89,7 @@ sealed class MainScreenBottomSheetContentState { @Composable fun MainScreenBottomSheetContentState( - settingsViewModel: SettingsViewModel, - mainViewModel: MainViewModel, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel, infoContentState: MainScreenBottomSheetContentState?, mainNavController: NavController, @@ -156,7 +150,7 @@ fun MainScreenBottomSheetContentState( ) is MainScreenBottomSheetContentState.EditOrAddProfileName -> ProfileSheetContent( - settingsViewModel = settingsViewModel, + settingsController = settingsController, profileSettingsViewModel = profileSettingsViewModel, addProfile = it.addProfile, profileToEdit = if (!it.addProfile) { @@ -168,7 +162,7 @@ fun MainScreenBottomSheetContentState( ConnectBottomSheetContent( onClickConnect = { scope.launch { - mainViewModel.welcomeDrawerShown() + settingsController.welcomeDrawerShown() } mainNavController.navigate( MainNavigationScreens.CardWall.path(profileHandler.activeProfile.id) @@ -176,7 +170,7 @@ fun MainScreenBottomSheetContentState( }, onCancel = { scope.launch { - mainViewModel.welcomeDrawerShown() + settingsController.welcomeDrawerShown() } onCancel() } @@ -190,19 +184,15 @@ fun MainScreenBottomSheetContentState( @OptIn(ExperimentalComposeUiApi::class) @Composable fun ProfileSheetContent( - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel, profileToEdit: ProfilesUseCaseData.Profile?, addProfile: Boolean = false, onCancel: () -> Unit ) { val keyboardController = LocalSoftwareKeyboardController.current - - val settingsScreenState by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } + val scope = rememberCoroutineScope() + val profilesState by settingsController.profilesState var textValue by remember { mutableStateOf(profileToEdit?.name ?: "") } var duplicated by remember { mutableStateOf(false) } @@ -210,7 +200,9 @@ fun ProfileSheetContent( if (!addProfile) { profileToEdit?.let { profileSettingsViewModel.updateProfileName(it.id, textValue) } } else { - settingsViewModel.addProfile(textValue) + scope.launch { + settingsController.addProfile(textValue) + } } onCancel() keyboardController?.hide() @@ -228,7 +220,7 @@ fun ProfileSheetContent( val name = sanitizeProfileName(it.trimStart()) textValue = name duplicated = textValue.trim() != profileToEdit?.name && - settingsScreenState.containsProfileWithName(textValue) + profilesState.containsProfileWithName(textValue) }, keyboardOptions = KeyboardOptions( autoCorrect = true, diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt index a4e971ed..f63113f4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt @@ -58,7 +58,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState 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 @@ -88,13 +87,10 @@ import de.gematik.ti.erp.app.analytics.TrackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.ui.CardWallScreen -import de.gematik.ti.erp.app.core.MainViewModel import de.gematik.ti.erp.app.debug.ui.DebugScreenWrapper import de.gematik.ti.erp.app.license.ui.LicenseScreen import de.gematik.ti.erp.app.onboarding.ui.OnboardingNavigationScreens import de.gematik.ti.erp.app.onboarding.ui.OnboardingScreen -import de.gematik.ti.erp.app.onboarding.ui.OnboardingSecureAppMethod -import de.gematik.ti.erp.app.onboarding.ui.ReturningUserSecureAppOnboardingScreen import de.gematik.ti.erp.app.orderhealthcard.ui.HealthCardContactOrderScreen import de.gematik.ti.erp.app.orders.ui.MessageScreen import de.gematik.ti.erp.app.orders.ui.OrderScreen @@ -104,9 +100,9 @@ import de.gematik.ti.erp.app.prescription.ui.ArchiveScreen import de.gematik.ti.erp.app.prescription.ui.MlKitInformationScreen import de.gematik.ti.erp.app.prescription.ui.MlKitIntroScreen import de.gematik.ti.erp.app.prescription.ui.PrescriptionScreen -import de.gematik.ti.erp.app.prescription.ui.PrescriptionViewModel import de.gematik.ti.erp.app.prescription.ui.ScanPrescriptionViewModel import de.gematik.ti.erp.app.prescription.ui.ScanScreen +import de.gematik.ti.erp.app.prescription.ui.rememberPrescriptionState import de.gematik.ti.erp.app.profiles.ui.DefaultProfile import de.gematik.ti.erp.app.profiles.ui.EditProfileScreen import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler @@ -114,13 +110,12 @@ import de.gematik.ti.erp.app.profiles.ui.ProfileImageCropper import de.gematik.ti.erp.app.profiles.ui.ProfileSettingsViewModel import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.redeem.ui.RedeemNavigation -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.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.SettingsViewModel import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.BottomNavigation @@ -144,18 +139,14 @@ import org.kodein.di.compose.rememberViewModel @Composable fun MainScreen( navController: NavHostController, - mainViewModel: MainViewModel, - mainScreenViewModel: MainScreenViewModel, - settingsViewModel: SettingsViewModel, + mainScreenController: MainScreenController, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel ) { - CheckAuthenticationMethod(mainViewModel, navController) - - val startDestination = determineStartDestination(mainViewModel) + val startDestination = determineStartDestination(settingsController) TrackNavigationChanges(navController) val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) - var secureMethod by rememberSaveable { mutableStateOf(OnboardingSecureAppMethod.None) } NavHost( navController, startDestination = startDestination @@ -163,15 +154,7 @@ fun MainScreen( composable(MainNavigationScreens.Onboarding.route) { OnboardingScreen( mainNavController = navController, - settingsViewModel = settingsViewModel - ) - } - composable(MainNavigationScreens.ReturningUserSecureAppOnboarding.route) { - ReturningUserSecureAppOnboardingScreen( - navController, - secureMethod = secureMethod, - onSecureMethodChange = { secureMethod = it }, - settingsViewModel = settingsViewModel + settingsController = settingsController ) } composable(OnboardingNavigationScreens.Biometry.route) { @@ -179,7 +162,7 @@ fun MainScreen( AllowBiometryScreen( onBack = { navController.popBackStack() }, onNext = { navController.popBackStack() }, - onSecureMethodChange = { secureMethod = it } + onSecureMethodChange = { } ) } } @@ -198,7 +181,7 @@ fun MainScreen( ) { SettingsScreen( mainNavController = navController, - settingsViewModel = settingsViewModel + settingsController = settingsController ) } composable(MainNavigationScreens.Camera.route) { @@ -208,9 +191,8 @@ fun MainScreen( composable(MainNavigationScreens.Prescriptions.route) { MainScreenWithScaffold( mainNavController = navController, - mainViewModel = mainViewModel, - mainScreenViewModel = mainScreenViewModel, - settingsViewModel = settingsViewModel, + mainScreenController = mainScreenController, + settingsController = settingsController, profileSettingsViewModel = profileSettingsViewModel ) } @@ -227,7 +209,7 @@ fun MainScreen( MainNavigationScreens.Pharmacies.arguments ) { PharmacyNavigation( - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onBack = { navController.popBackStack() }, @@ -241,7 +223,7 @@ fun MainScreen( composable(MainNavigationScreens.InsecureDeviceScreen.route) { InsecureDeviceScreen( navController, - mainViewModel, + settingsController, stringResource(id = R.string.insecure_device_title), painterResource(id = R.drawable.laptop_woman_yellow), stringResource(id = R.string.insecure_device_header), @@ -252,7 +234,7 @@ fun MainScreen( composable(MainNavigationScreens.MlKitIntroScreen.route) { MlKitIntroScreen( navController, - mainViewModel + settingsController ) } composable(MainNavigationScreens.MlKitInformationScreen.route) { @@ -263,7 +245,7 @@ fun MainScreen( composable(MainNavigationScreens.IntegrityNotOkScreen.route) { InsecureDeviceScreen( navController, - mainViewModel, + settingsController, stringResource(id = R.string.insecure_device_title_safetynet), painterResource(id = R.drawable.laptop_woman_pink), stringResource(id = R.string.insecure_device_header_safetynet), @@ -277,7 +259,7 @@ fun MainScreen( MainNavigationScreens.Redeem.arguments ) { RedeemNavigation( - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onFinish = { navController.popBackStack(MainNavigationScreens.Prescriptions.route, false) } @@ -324,7 +306,7 @@ fun MainScreen( remember { navController.currentBackStackEntry?.arguments?.getString("profileId")!! } EditProfileScreen( profileId, - settingsViewModel, + settingsController, profileSettingsViewModel, onBack = { navController.popBackStack() }, mainNavController = navController @@ -378,9 +360,9 @@ fun MainScreen( onBack = { navController.popBackStack() }, onAllowAnalytics = { if (it) { - settingsViewModel.onTrackingAllowed() + settingsController.onTrackingAllowed() } else { - settingsViewModel.onTrackingDisallowed() + settingsController.onTrackingDisallowed() } } ) @@ -390,7 +372,7 @@ fun MainScreen( NavigationAnimation(mode = navigationMode) { SecureAppWithPassword( navController, - settingsViewModel + settingsController ) } } @@ -402,21 +384,19 @@ fun MainScreen( MainNavigationScreens.EditProfile.arguments ) { val profileId = remember { it.arguments!!.getString("profileId")!! } + val scope = rememberCoroutineScope() + val profilesState by settingsController.profilesState - val state by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } - - state.profileById(profileId)?.let { profile -> + profilesState.profileById(profileId)?.let { profile -> EditProfileScreen( - state, + profilesState, profile, - settingsViewModel, + settingsController, profileSettingsViewModel, onRemoveProfile = { - settingsViewModel.removeProfile(profile, it) + scope.launch { + settingsController.removeProfile(profile, it) + } navController.popBackStack() }, onBack = { navController.popBackStack() }, @@ -469,10 +449,10 @@ fun MainScreen( composable( MainNavigationScreens.Archive.route ) { - val prescriptionViewModel by rememberViewModel() + val prescriptionState = rememberPrescriptionState() NavigationAnimation(mode = navigationMode) { - ArchiveScreen(prescriptionViewModel = prescriptionViewModel, navController = navController) { + ArchiveScreen(prescriptionState = prescriptionState, navController = navController) { navController.popBackStack() } } @@ -481,9 +461,9 @@ fun MainScreen( } @Composable -private fun determineStartDestination(mainViewModel: MainViewModel) = +private fun determineStartDestination(settingsController: SettingsController) = when { - mainViewModel.showOnboarding -> { + settingsController.showOnboarding -> { MainNavigationScreens.Onboarding.route } @@ -492,33 +472,12 @@ private fun determineStartDestination(mainViewModel: MainViewModel) = } } -@Composable -private fun CheckAuthenticationMethod(mainViewModel: MainViewModel, navController: NavHostController) { - LaunchedEffect(Unit) { - mainViewModel.authenticationMethod.collect { - if (!mainViewModel.showOnboarding && !( - it is SettingsData.AuthenticationMode.Password || - it == SettingsData.AuthenticationMode.DeviceSecurity - ) - ) { - navController.navigate(MainNavigationScreens.ReturningUserSecureAppOnboarding.path()) { - launchSingleTop = true - popUpTo(MainNavigationScreens.Prescriptions.path()) { - inclusive = true - } - } - } - } - } -} - @OptIn(ExperimentalMaterialApi::class) @Composable private fun MainScreenWithScaffold( mainNavController: NavController, - mainViewModel: MainViewModel, - mainScreenViewModel: MainScreenViewModel, - settingsViewModel: SettingsViewModel, + mainScreenController: MainScreenController, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel ) { val context = LocalContext.current @@ -532,17 +491,17 @@ private fun MainScreenWithScaffold( } } - CheckInsecureDevice(mainViewModel, mainNavController) - CheckDeviceIntegrity(mainViewModel, mainNavController) + CheckInsecureDevice(settingsController, mainNavController) + CheckDeviceIntegrity(mainScreenController, mainNavController) val scaffoldState = rememberScaffoldState() MainScreenSnackbar( - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, scaffoldState = scaffoldState ) - OrderSuccessHandler(mainScreenViewModel) + OrderSuccessHandler(mainScreenController) var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } @@ -566,7 +525,7 @@ private fun MainScreenWithScaffold( } LaunchedEffect(Unit) { - if (mainViewModel.showWelcomeDrawer.first()) { + if (settingsController.showWelcomeDrawer.first()) { mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Connect } } @@ -574,15 +533,15 @@ private fun MainScreenWithScaffold( LaunchedEffect(sheetState.isVisible) { if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Connect) { - mainViewModel.welcomeDrawerShown() + settingsController.welcomeDrawerShown() } mainScreenBottomSheetContentState = null } } LaunchedEffect(Unit) { - if (mainViewModel.talkbackEnabled(context)) { - mainViewModel.mainScreenTooltipsShown() + if (settingsController.talkbackEnabled(context)) { + settingsController.mainScreenTooltipsShown() } } @@ -594,7 +553,7 @@ private fun MainScreenWithScaffold( mutableStateOf>(emptyMap()) } - ToolTips(mainViewModel, isInPrescriptionScreen, toolTipBounds) + ToolTips(settingsController, isInPrescriptionScreen, toolTipBounds) val coroutineScope = rememberCoroutineScope() @@ -612,8 +571,7 @@ private fun MainScreenWithScaffold( sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) }, sheetContent = { MainScreenBottomSheetContentState( - settingsViewModel = settingsViewModel, - mainViewModel = mainViewModel, + settingsController = settingsController, profileSettingsViewModel = profileSettingsViewModel, infoContentState = mainScreenBottomSheetContentState, mainNavController = mainNavController, @@ -630,9 +588,8 @@ private fun MainScreenWithScaffold( ExternalAuthenticationDialog() MainScreenScaffold( - mainViewModel = mainViewModel, - mainScreenViewModel = mainScreenViewModel, - settingsViewModel = settingsViewModel, + mainScreenController = mainScreenController, + settingsController = settingsController, mainNavController = mainNavController, bottomNavController = bottomNavController, tooltipBounds = toolTipBounds, @@ -656,9 +613,8 @@ private fun MainScreenWithScaffold( @Composable private fun MainScreenScaffold( - mainViewModel: MainViewModel, - mainScreenViewModel: MainScreenViewModel, - settingsViewModel: SettingsViewModel, + mainScreenController: MainScreenController, + settingsController: SettingsController, mainNavController: NavController, bottomNavController: NavHostController, tooltipBounds: MutableState>, @@ -683,8 +639,8 @@ private fun MainScreenScaffold( MultiProfileTopAppBar( navController = mainNavController, elevated = topBarElevated, - mainScreenViewModel = mainScreenViewModel, - mainViewModel = mainViewModel, + settingsController = settingsController, + mainScreenController = mainScreenController, isInPrescriptionScreen = isInPrescriptionScreen, onClickAddProfile = onClickAddProfile, onClickChangeProfileName = onClickChangeProfileName, @@ -695,7 +651,7 @@ private fun MainScreenScaffold( bottomBar = { MainScreenBottomBar( navController = mainNavController, - viewModel = mainScreenViewModel, + mainScreenController = mainScreenController, bottomNavController = bottomNavController ) }, @@ -712,8 +668,8 @@ private fun MainScreenScaffold( ) { innerPadding -> MainScreenBottomNavHost( - mainScreenViewModel = mainScreenViewModel, - settingsViewModel = settingsViewModel, + mainScreenController = mainScreenController, + settingsController = settingsController, mainNavController = mainNavController, bottomNavController = bottomNavController, innerPadding = innerPadding, @@ -728,8 +684,8 @@ private fun MainScreenScaffold( @Composable private fun MainScreenBottomNavHost( - mainScreenViewModel: MainScreenViewModel, - settingsViewModel: SettingsViewModel, + mainScreenController: MainScreenController, + settingsController: SettingsController, mainNavController: NavController, bottomNavController: NavHostController, innerPadding: PaddingValues, @@ -747,12 +703,12 @@ private fun MainScreenBottomNavHost( startDestination = MainNavigationScreens.Prescriptions.path() ) { composable(MainNavigationScreens.Prescriptions.route) { - val prescriptionViewModel by rememberViewModel() + val prescriptionState = rememberPrescriptionState() PrescriptionScreen( navController = mainNavController, onClickAvatar = onClickAvatar, - prescriptionViewModel = prescriptionViewModel, - mainScreenViewModel = mainScreenViewModel, + prescriptionState = prescriptionState, + mainScreenController = mainScreenController, onElevateTopBar = onElevateTopBar, onClickArchive = onClickArchive ) @@ -760,7 +716,7 @@ private fun MainScreenBottomNavHost( composable(MainNavigationScreens.Orders.route) { OrderScreen( mainNavController = mainNavController, - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onElevateTopBar = onElevateTopBar ) } @@ -770,7 +726,7 @@ private fun MainScreenBottomNavHost( ) { SettingsScreen( mainNavController = mainNavController, - settingsViewModel = settingsViewModel + settingsController = settingsController ) } } @@ -778,12 +734,12 @@ private fun MainScreenBottomNavHost( } @Composable -private fun CheckDeviceIntegrity(mainViewModel: MainViewModel, mainNavController: NavController) { +private fun CheckDeviceIntegrity(mainScreenController: MainScreenController, mainNavController: NavController) { LaunchedEffect(Unit) { if (BuildConfig.DEBUG) { return@LaunchedEffect } - if (!mainViewModel.checkDeviceIntegrity().first()) { + if (!mainScreenController.checkDeviceIntegrity().first()) { withContext(Dispatchers.Main) { mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) navOptions { @@ -798,10 +754,10 @@ private fun CheckDeviceIntegrity(mainViewModel: MainViewModel, mainNavController } @Composable -private fun CheckInsecureDevice(mainViewModel: MainViewModel, mainNavController: NavController) { +private fun CheckInsecureDevice(settingsController: SettingsController, mainNavController: NavController) { LaunchedEffect(Unit) { withContext(Dispatchers.Main) { - if (mainViewModel.showInsecureDevicePrompt.first()) { + if (settingsController.showInsecureDevicePrompt.first()) { mainNavController.navigate(MainNavigationScreens.InsecureDeviceScreen.path()) navOptions { launchSingleTop = true @@ -818,14 +774,14 @@ private fun CheckInsecureDevice(mainViewModel: MainViewModel, mainNavController: private fun MainScreenBottomBar( navController: NavController, bottomNavController: NavController, - viewModel: MainScreenViewModel + mainScreenController: MainScreenController ) { val navBackStackEntry by bottomNavController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route val profileHandler = LocalProfileHandler.current val profileId = profileHandler.activeProfile.id - val unreadMessagesAvailable by viewModel.unreadMessagesAvailable(profileId) + val unreadMessagesAvailable by mainScreenController.unreadMessagesAvailable(profileId) .collectAsState(initial = false) BottomNavigation( @@ -919,7 +875,7 @@ private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { @Composable private fun ProfilesChipBar( - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, onClickAddProfile: () -> Unit, onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, tooltipBounds: MutableState>, @@ -956,7 +912,7 @@ private fun ProfilesChipBar( item { ProfileChip( profile = profile, - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, selected = profile.id == profileHandler.activeProfile.id, onClickChip = { scope.launch { profileHandler.switchActiveProfile(profile) } }, onClickChangeProfileName = onClickChangeProfileName, @@ -983,8 +939,8 @@ private fun ProfilesChipBar( @Composable private fun MultiProfileTopAppBar( navController: NavController, - mainScreenViewModel: MainScreenViewModel, - mainViewModel: MainViewModel, + mainScreenController: MainScreenController, + settingsController: SettingsController, isInPrescriptionScreen: Boolean, elevated: Boolean, onClickAddProfile: () -> Unit, @@ -995,7 +951,7 @@ private fun MultiProfileTopAppBar( val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else 0.dp } val toolTipBoundsRequired by produceState(initialValue = false) { - mainViewModel.showMainScreenToolTips().collect { + settingsController.showMainScreenToolTips().collect { value = it } } @@ -1014,7 +970,7 @@ private fun MultiProfileTopAppBar( IconButton( onClick = { scope.launch { - if (mainViewModel.mlKitNotAccepted().first()) { + if (settingsController.mlKitNotAccepted().first()) { navController.navigate(MainNavigationScreens.MlKitIntroScreen.path()) } else { navController.navigate(MainNavigationScreens.Camera.path()) @@ -1041,7 +997,7 @@ private fun MultiProfileTopAppBar( }, content = { ProfilesChipBar( - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onClickAddProfile = onClickAddProfile, onClickChangeProfileName = onClickChangeProfileName, tooltipBounds = tooltipBounds, diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt similarity index 67% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenViewModel.kt rename to android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt index cb6811c2..2b16a614 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenViewModel.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt @@ -18,20 +18,24 @@ package de.gematik.ti.erp.app.mainscreen.ui +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel +import de.gematik.ti.erp.app.attestation.usecase.IntegrityUseCase 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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.map +import org.kodein.di.compose.rememberInstance -// TODO: transform to controller like class -class MainScreenViewModel( +class MainScreenController( + private val integrityUseCase: IntegrityUseCase, private val messageUseCase: OrderUseCase -) : ViewModel() { +) { enum class OrderedEvent { Success, @@ -59,4 +63,28 @@ class MainScreenViewModel( fun onOrdered(hasError: Boolean) { orderedEvent = if (hasError) OrderedEvent.Error else OrderedEvent.Success } + + var integrityPromptShown = false + + fun checkDeviceIntegrity() = integrityUseCase.runIntegrityAttestation().map { + if (!it && !integrityPromptShown) { + integrityPromptShown = true + false + } else { + true + } + } +} + +@Composable +fun rememberMainScreenController(): MainScreenController { + val integrityUseCase by rememberInstance() + val messageUseCase by rememberInstance() + + return remember { + MainScreenController( + integrityUseCase = integrityUseCase, + messageUseCase = messageUseCase + ) + } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt index c710efc6..7e4aab1e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt @@ -33,7 +33,6 @@ data class TaskIds(val ids: List) : Parcelable, List by ids object MainNavigationScreens { object Onboarding : Route("Onboarding") - object ReturningUserSecureAppOnboarding : Route("ReturningUserSecureAppOnboarding") object Biometry : Route("Biometry") object Settings : Route("Settings") object Camera : Route("Camera") diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt index 6a5803dc..6c913fc1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt @@ -28,30 +28,30 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.prescription.ui.GenerellErrorState +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.ui.RefreshedState import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource @Composable fun MainScreenSnackbar( - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, scaffoldState: ScaffoldState ) { var refreshEvent by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - mainScreenViewModel.onRefreshEvent.collect { + mainScreenController.onRefreshEvent.collect { refreshEvent = it } } val refreshEventText = refreshEvent?.let { when (it) { - GenerellErrorState.NetworkNotAvailable -> + GeneralErrorState.NetworkNotAvailable -> stringResource(R.string.error_message_network_not_available) - is GenerellErrorState.ServerCommunicationFailedWhileRefreshing -> + is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> stringResource(R.string.error_message_server_communication_failed).format(it.code) - GenerellErrorState.FatalTruststoreState -> + GeneralErrorState.FatalTruststoreState -> stringResource(R.string.error_message_vau_error) is RefreshedState -> { if (it.nrOfNewPrescriptions == 0) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt index b2dbb94a..1c61272c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt @@ -30,25 +30,25 @@ import de.gematik.ti.erp.app.utils.compose.AcceptDialog @Composable fun OrderSuccessHandler( - mainScreenVM: MainScreenViewModel + mainScreenController: MainScreenController ) { val context = LocalContext.current - when (mainScreenVM.orderedEvent) { - MainScreenViewModel.OrderedEvent.Success -> { + when (mainScreenController.orderedEvent) { + MainScreenController.OrderedEvent.Success -> { LaunchedEffect(Unit) { requestReview(context) - mainScreenVM.resetOrderedEvent() + mainScreenController.resetOrderedEvent() } } - MainScreenViewModel.OrderedEvent.Error -> { + MainScreenController.OrderedEvent.Error -> { AcceptDialog( header = stringResource(R.string.pharmacy_order_not_possible_title), info = stringResource(R.string.pharmacy_order_not_possible_desc), acceptText = stringResource(R.string.ok), onClickAccept = { - mainScreenVM.resetOrderedEvent() + mainScreenController.resetOrderedEvent() } ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt index 11eae86b..a96800ef 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt @@ -128,19 +128,19 @@ fun AddProfileChip( fun ProfileChip( profile: ProfilesUseCaseData.Profile, selected: Boolean, - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, onClickChip: (ProfileIdentifier) -> Unit, onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, tooltipBounds: MutableState>, toolTipBoundsRequired: Boolean ) { - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenViewModel) + val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) val isRefreshing by refreshPrescriptionsController.isRefreshing var refreshEvent by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - mainScreenViewModel.onRefreshEvent.collect { + mainScreenController.onRefreshEvent.collect { refreshEvent = it } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt index d89f2894..5ce90e76 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt @@ -42,14 +42,14 @@ private const val SpinnerDelay = 300L fun RefreshScaffold( profileId: ProfileIdentifier, onUserNotAuthenticated: () -> Unit, - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, onShowCardWall: () -> Unit, content: @Composable (onRefresh: (isUserAction: Boolean, priority: MutatePriority) -> Unit) -> Unit ) { val scope = rememberCoroutineScope() val mutex = MutatorMutex() - val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenViewModel) + val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) val isRefreshing by refreshPrescriptionsController.isRefreshing val refreshState = rememberSwipeRefreshState(isRefreshing) diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt index 5990ef1e..0b11f311 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.core.MainViewModel +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 @@ -73,7 +73,7 @@ data class ToolTipState( @Composable fun ToolTips( - mainViewModel: MainViewModel, + settingsController: SettingsController, isInPrescriptionScreen: Boolean, toolTipBounds: MutableState> ) { @@ -82,7 +82,7 @@ fun ToolTips( var tooltipNr by remember { mutableStateOf(0) } val showMainScreenTooltips by produceState(initialValue = false) { - mainViewModel.showMainScreenToolTips().collect { + settingsController.showMainScreenToolTips().collect { value = it } } @@ -113,7 +113,7 @@ fun ToolTips( 2 -> ToolTip( onDismissRequest = { coroutineScope.launch { - mainViewModel.mainScreenTooltipsShown() + settingsController.mainScreenTooltipsShown() } }, tooltipState = ToolTipState( diff --git a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt index 36c2ba49..368cf1ea 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt @@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset @@ -43,10 +42,7 @@ 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.Button import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.icons.Icons @@ -75,7 +71,6 @@ import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.icons.rounded.FlashOn import androidx.compose.material.icons.rounded.PersonPin @@ -88,10 +83,9 @@ import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens 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.SettingsViewModel +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.BottomAppBar import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.OutlinedDebugButton import de.gematik.ti.erp.app.utils.compose.SecondaryButton @@ -107,7 +101,6 @@ import de.gematik.ti.erp.app.webview.WebViewScreen import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.Locale import kotlin.math.max import kotlin.math.min @@ -135,74 +128,10 @@ private enum class OnboardingPages(val index: Int) { } } -@Composable -fun ReturningUserSecureAppOnboardingScreen( - mainNavController: NavController, - settingsViewModel: SettingsViewModel, - secureMethod: OnboardingSecureAppMethod, - onSecureMethodChange: (OnboardingSecureAppMethod) -> Unit -) { - val enabled = when (secureMethod) { - is OnboardingSecureAppMethod.DeviceSecurity -> true - is OnboardingSecureAppMethod.Password -> (secureMethod as? OnboardingSecureAppMethod.Password)?.let { - it.checkedPassword != null - } ?: false - - else -> false - } - - val coroutineScope = rememberCoroutineScope() - Scaffold( - modifier = Modifier.statusBarsPadding(), - bottomBar = { - BottomAppBar(backgroundColor = MaterialTheme.colors.surface) { - Spacer(modifier = Modifier.weight(1f)) - Button( - enabled = enabled, - onClick = { - coroutineScope.launch { - when (val sm = secureMethod) { - is OnboardingSecureAppMethod.DeviceSecurity -> - settingsViewModel.onSelectDeviceSecurityAuthenticationMode() - - is OnboardingSecureAppMethod.Password -> - settingsViewModel.onSelectPasswordAsAuthenticationMode( - requireNotNull(sm.checkedPassword) - ) - - else -> error("Illegal state. Authentication must be set") - } - mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) { - launchSingleTop = true - popUpTo(MainNavigationScreens.ReturningUserSecureAppOnboarding.path()) { - inclusive = true - } - } - } - }, - shape = RoundedCornerShape(PaddingDefaults.Small) - ) { - Text(stringResource(R.string.ok).uppercase(Locale.getDefault())) - } - SpacerMedium() - } - } - ) { innerPadding -> - Box(Modifier.padding(innerPadding)) { - OnboardingSecureApp( - secureMethod = secureMethod, - onSecureMethodChange = onSecureMethodChange, - onNextPage = {}, - onOpenBiometricScreen = { mainNavController.navigate(MainNavigationScreens.Biometry.path()) } - ) - } - } -} - @Composable fun OnboardingScreen( mainNavController: NavController, - settingsViewModel: SettingsViewModel + settingsController: SettingsController ) { val navController = rememberNavController() val coroutineScope = rememberCoroutineScope() @@ -230,7 +159,7 @@ fun OnboardingScreen( }, onSaveNewUser = { allowTracking, defaultProfileName, secureMethod -> coroutineScope.launch(Dispatchers.Main) { - settingsViewModel.onboardingSucceeded( + settingsController.onboardingSucceeded( authenticationMode = when (secureMethod) { is OnboardingSecureAppMethod.DeviceSecurity -> SettingsData.AuthenticationMode.DeviceSecurity diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt index d1d41f3d..c4314506 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt @@ -87,7 +87,7 @@ import androidx.navigation.NavController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold import de.gematik.ti.erp.app.orders.usecase.OrderUseCase import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData @@ -126,7 +126,7 @@ import java.time.format.FormatStyle @Composable fun OrderScreen( mainNavController: NavController, - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, onElevateTopBar: (Boolean) -> Unit ) { val profileHandler = LocalProfileHandler.current @@ -148,7 +148,7 @@ fun OrderScreen( RefreshScaffold( profileId = profileHandler.activeProfile.id, onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onShowCardWall = onShowCardWall ) { onRefresh -> Orders( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt index 8d6cbacf..322f8e9a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Details.kt @@ -91,11 +91,9 @@ import de.gematik.ti.erp.app.utils.compose.HintCard import de.gematik.ti.erp.app.utils.compose.HintCardDefaults import de.gematik.ti.erp.app.utils.compose.HintSmallImage import de.gematik.ti.erp.app.utils.compose.HintTextActionButton -import de.gematik.ti.erp.app.utils.compose.Spacer16 -import de.gematik.ti.erp.app.utils.compose.Spacer4 -import de.gematik.ti.erp.app.utils.compose.Spacer8 -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.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge import de.gematik.ti.erp.app.utils.compose.TertiaryButton import de.gematik.ti.erp.app.utils.compose.canHandleIntent @@ -469,7 +467,7 @@ private fun PharmacyOpeningHours(openingHours: OpeningHours) { } } } - Spacer16() + SpacerMedium() } } } @@ -536,7 +534,7 @@ private fun DataInfoSection(modifier: Modifier) { } } ) - Spacer8() + SpacerSmall() Row(modifier = modifier) { HintTextActionButton(text = stringResource(R.string.pharmacy_detail_data_info_btn)) { uriHandler.openUri(uriFaq) @@ -574,7 +572,7 @@ private fun Label( style = AppTheme.typography.body1, color = AppTheme.colors.primary600 ) - Spacer4() + SpacerTiny() Text( text = label, style = AppTheme.typography.body2l diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt index 53470888..7573ef95 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt @@ -24,7 +24,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -106,7 +105,7 @@ import de.gematik.ti.erp.app.core.complexAutoSaver import de.gematik.ti.erp.app.fhir.model.Location import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.prescription.ui.GenerellErrorState +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet @@ -205,7 +204,7 @@ fun MapsOverview( is PharmacySearchController.State.Pharmacies -> pharmacies = result.pharmacies - is GenerellErrorState -> + is GeneralErrorState -> mapsErrorMessage(context, result)?.let { scaffoldState.snackbarHostState.showSnackbar(it) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt index fb8ec146..50e9bb32 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt @@ -20,12 +20,12 @@ package de.gematik.ti.erp.app.pharmacy.ui import android.content.Context import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.prescription.ui.GenerellErrorState +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState fun mapsErrorMessage(context: Context, deleteState: PrescriptionServiceErrorState): String? = when (deleteState) { - GenerellErrorState.NetworkNotAvailable -> + GeneralErrorState.NetworkNotAvailable -> context.getString(R.string.error_message_network_not_available) else -> null } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt index 14617f59..a9cdcf17 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt @@ -31,7 +31,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyNavigationScreens import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationMode import de.gematik.ti.erp.app.utils.compose.navigationModeState @@ -41,7 +41,7 @@ import kotlinx.coroutines.launch @Suppress("LongMethod") @Composable fun PharmacyNavigation( - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, orderState: PharmacyOrderState = rememberPharmacyOrderState(), isNestedNavigation: Boolean = false, onBack: () -> Unit, @@ -226,7 +226,7 @@ fun PharmacyNavigation( navController.navigate(PharmacyNavigationScreens.PrescriptionSelection.path()) }, onFinish = { hasError -> - mainScreenViewModel.onOrdered(hasError = hasError) + mainScreenController.onOrdered(hasError = hasError) onFinish() } ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt index 657a0bbd..a5c1dc71 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt @@ -20,20 +20,20 @@ package de.gematik.ti.erp.app.pharmacy.ui import android.content.Context import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.prescription.ui.GenerellErrorState +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState fun redeemErrorMessage(context: Context, redeemState: PrescriptionServiceErrorState): String? = when (redeemState) { - GenerellErrorState.NetworkNotAvailable -> + GeneralErrorState.NetworkNotAvailable -> context.getString(R.string.error_message_network_not_available) - is GenerellErrorState.ServerCommunicationFailedWhileRefreshing -> + is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> context.getString(R.string.error_message_server_communication_failed).format(redeemState.code) - GenerellErrorState.FatalTruststoreState -> + GeneralErrorState.FatalTruststoreState -> context.getString(R.string.error_message_vau_error) is RedeemPrescriptionsController.State.Error.Unknown -> context.getString(R.string.redeem_online_error_uploading) - is GenerellErrorState.NoneEnrolled -> + is GeneralErrorState.NoneEnrolled -> context.getString(R.string.no_auth_enrolled) else -> null } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt index 08c3b138..c4bcb31f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt @@ -21,10 +21,10 @@ package de.gematik.ti.erp.app.pharmacy.ui.model import de.gematik.ti.erp.app.Route object PharmacyNavigationScreens { - object StartSearch : Route("StartSearch") - object List : Route("List") - object Maps : Route("Maps") - object OrderOverview : Route("OrderOverview") - object EditShippingContact : Route("EditShippingContact") - object PrescriptionSelection : Route("PrescriptionSelection") + object StartSearch : Route("pharmacy_start_search") + object List : Route("pharmacy_list") + object Maps : Route("pharmacy_maps") + object OrderOverview : Route("pharmacy_order_overview") + object EditShippingContact : Route("pharmacy_edit_shipping_contact") + object PrescriptionSelection : Route("pharmacy_prescription_selection") } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt b/android/src/main/java/de/gematik/ti/erp/app/pkv/PkvModule.kt similarity index 57% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt rename to android/src/main/java/de/gematik/ti/erp/app/pkv/PkvModule.kt index 6a912338..3f894718 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/AlgorithmIdentifiersExtending.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/pkv/PkvModule.kt @@ -16,10 +16,18 @@ * */ -package de.gematik.ti.erp.app.idp +package de.gematik.ti.erp.app.pkv -object AlgorithmIdentifiersExtending { - const val BRAINPOOL256_USING_SHA256 = "BP256R1" - const val BRAINPOOL384_USING_SHA384 = "BP384R1" - const val BRAINPOOL512_USING_SHA512 = "BP512R1" +import de.gematik.ti.erp.app.consent.repository.ConsentRemoteDataSource +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.consent.usecase.ConsentUseCase + +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val pkvModule = DI.Module("pkvModule") { + bindProvider { ConsentUseCase(instance()) } + bindProvider { ConsentRemoteDataSource(instance()) } + bindProvider { ConsentRepository(instance(), instance()) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt b/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt new file mode 100644 index 00000000..0d0ace5f --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv.ui + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator + +import de.gematik.ti.erp.app.consent.usecase.ConsentUseCase +import de.gematik.ti.erp.app.core.LocalAuthenticator +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState +import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState +import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState +import de.gematik.ti.erp.app.prescription.ui.catchAndTransformRemoteExceptions +import de.gematik.ti.erp.app.prescription.ui.retryWithAuthenticator + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +import kotlinx.coroutines.Dispatchers + +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.kodein.di.compose.rememberInstance +import java.net.HttpURLConnection + +@Stable +class ConsentController( + val context: Context, + val profileId: ProfileIdentifier, + val insuranceIdentifier: String, + private val authenticator: Authenticator, + private val useCase: ConsentUseCase, + val dispatchers: DispatchProvider +) { + + sealed interface State : PrescriptionServiceState { + object ChargeConsentNotGranted : State + object ChargeConsentGranted : State + object ChargeConsentRevoked : State + + sealed interface Error : State, PrescriptionServiceErrorState { + object ChargeConsentAlreadyGranted : Error + object ChargeConsentAlreadyRevoked : Error + } + fun PrescriptionServiceState.isConsentGranted() = + this == ChargeConsentGranted || this == Error.ChargeConsentAlreadyGranted + } + + fun getChargeConsent() = flow { + emit(useCase.getChargeConsent(profileId)) + }.map { result -> + result.map { + when (it) { + true -> State.ChargeConsentGranted + else -> State.ChargeConsentNotGranted + } + }.getOrThrow() + }.retryWithAuthenticator( + isUserAction = true, + authenticate = authenticator.authenticateForPrescriptions(profileId) + ) + .catchAndTransformRemoteExceptions() + .flowOn(Dispatchers.IO) + + fun grantChargeConsent() = flow { + emit(useCase.grantChargeConsent(profileId, insuranceIdentifier)) + }.map { result -> + result.fold( + onSuccess = { + State.ChargeConsentGranted + }, + onFailure = { + if (it is ApiCallException) { + when (it.response.code()) { + HttpURLConnection.HTTP_CONFLICT -> State.Error.ChargeConsentAlreadyGranted + else -> throw it + } + } else { + throw it + } + } + ) + }.retryWithAuthenticator( + isUserAction = true, + authenticate = authenticator.authenticateForPrescriptions(profileId) + ) + .catchAndTransformRemoteExceptions() + .flowOn(Dispatchers.IO) + + fun revokeChargeConsent() = flow { + emit(useCase.deleteChargeConsent(profileId)) + }.map { result -> + result.fold( + onSuccess = { + State.ChargeConsentRevoked + }, + onFailure = { + if (it is ApiCallException) { + when (it.response.code()) { + HttpURLConnection.HTTP_NOT_FOUND -> State.Error.ChargeConsentAlreadyRevoked + else -> throw it + } + } else { + throw it + } + } + ) + }.retryWithAuthenticator( + isUserAction = true, + authenticate = authenticator.authenticateForPrescriptions(profileId) + ) + .catchAndTransformRemoteExceptions() + .flowOn(Dispatchers.IO) +} + +@Composable +fun rememberConsentController(profile: ProfilesUseCaseData.Profile): ConsentController { + val context = LocalContext.current + val dispatchers by rememberInstance() + val consentUseCase by rememberInstance() + val authenticator = LocalAuthenticator.current + + return remember(profile.id, profile.insuranceInformation.insuranceIdentifier) { + ConsentController( + context = context, + profileId = profile.id, + insuranceIdentifier = profile.insuranceInformation.insuranceIdentifier, + useCase = consentUseCase, + authenticator = authenticator, + dispatchers = dispatchers + ) + } +} + +fun consentErrorMessage(context: Context, consentErrorState: PrescriptionServiceErrorState): String? = + when (consentErrorState) { + GeneralErrorState.NetworkNotAvailable -> + context.getString(R.string.error_message_network_not_available) + is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> + context.getString(R.string.error_message_server_communication_failed).format(consentErrorState.code) + GeneralErrorState.FatalTruststoreState -> + context.getString(R.string.error_message_vau_error) + else -> null + } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt new file mode 100644 index 00000000..4beeedcb --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.SnackbarHost +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController +import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold +import de.gematik.ti.erp.app.pkv.ui.ConsentController.State.ChargeConsentNotGranted.isConsentGranted +import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState +import de.gematik.ti.erp.app.prescription.ui.rememberRefreshPrescriptionsController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.visualTestTag +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +@Composable +fun InvoiceInformationScreen( + mainScreenController: MainScreenController, + onBack: () -> Unit, + selectedProfile: ProfilesUseCaseData.Profile, + onShowCardWall: () -> Unit +) { + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + + val consentController = rememberConsentController(profile = selectedProfile) + + val ssoTokenValid by remember(selectedProfile) { + derivedStateOf { + selectedProfile.ssoTokenValid() + } + } + + var consentGranted by remember { mutableStateOf(false) } + + var showGrantConsentDialog by remember { mutableStateOf(false) } + + val context = LocalContext.current + + LaunchedEffect(Unit) { + if (ssoTokenValid) { + val consentState = consentController.getChargeConsent().first() + when (consentState) { + is PrescriptionServiceErrorState -> { + consentErrorMessage(context, consentState)?.let { + scaffoldState.snackbarHostState.showSnackbar(it) + } + } + } + consentGranted = consentState.isConsentGranted() + showGrantConsentDialog = !consentGranted + } + } + + var connectBottomBarVisible by remember { mutableStateOf(!ssoTokenValid) } + + var showRevokeConsentAlert by remember { mutableStateOf(false) } + + if (showGrantConsentDialog && selectedProfile.ssoTokenValid()) { + GrantConsentDialog( + onCancel = onBack, + onGrantConsent = { + scope.launch { + val consentState = consentController.grantChargeConsent().first() + when (consentState) { + is PrescriptionServiceErrorState -> { + consentErrorMessage(context, consentState)?.let { + scaffoldState.snackbarHostState.showSnackbar(it) + } + } + } + consentGranted = consentState.isConsentGranted() + showGrantConsentDialog = false + } + } + ) + } + + if (showRevokeConsentAlert) { + RevokeConsentDialog( + onCancel = { showRevokeConsentAlert = false }, + onRevokeConsent = { + scope.launch { + val consentState = consentController.revokeChargeConsent().first() + when (consentState) { + is PrescriptionServiceErrorState -> { + consentErrorMessage(context, consentState)?.let { + scaffoldState.snackbarHostState.showSnackbar(it) + } + } + } + consentGranted = consentState.isConsentGranted() + onBack() + } + } + ) + } + + val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) + + AnimatedElevationScaffold( + modifier = Modifier + .imePadding() + .visualTestTag(TestTag.Profile.InvoicesScreen), + topBarTitle = stringResource(R.string.profile_invoices), + snackbarHost = { + SnackbarHost(it, modifier = Modifier.systemBarsPadding()) + }, + bottomBar = { + if (connectBottomBarVisible) { + ConnectBottomBar { + scope.launch { + refreshPrescriptionsController.refresh( + profileId = selectedProfile.id, + isUserAction = true, + onUserNotAuthenticated = { connectBottomBarVisible = true }, + onShowCardWall = onShowCardWall + ) + } + } + } + }, + navigationMode = NavigationBarMode.Back, + scaffoldState = scaffoldState, + listState = listState, + actions = { + InvoicesThreeDotMenu( + consentGranted = consentGranted, + onClickRevokeConsent = { showRevokeConsentAlert = true } + ) + }, + onBack = onBack + ) { + RefreshScaffold( + profileId = selectedProfile.id, + onUserNotAuthenticated = { connectBottomBarVisible = true }, + mainScreenController = mainScreenController, + onShowCardWall = {} + ) { _ -> + Invoices( + listState = listState + ) + } + } +} + +@Composable +fun RevokeConsentDialog(onCancel: () -> Unit, onRevokeConsent: () -> Unit) { + CommonAlertDialog( + header = stringResource(R.string.profile_revoke_consent_header), + info = stringResource(R.string.profile_revoke_consent_info), + cancelText = stringResource(R.string.profile_invoices_cancel), + actionText = stringResource(R.string.profile_revoke_consent), + onCancel = onCancel, + onClickAction = onRevokeConsent + ) +} + +@Composable +fun GrantConsentDialog(onCancel: () -> Unit, onGrantConsent: () -> Unit) { + CommonAlertDialog( + header = stringResource(R.string.profile_grant_consent_header), + info = stringResource(R.string.profile_grant_consent_info), + cancelText = stringResource(R.string.profile_invoices_cancel), + actionText = stringResource(R.string.profile_grant_consent), + onCancel = onCancel, + onClickAction = onGrantConsent + ) +} + +@Composable +fun InvoicesThreeDotMenu(consentGranted: Boolean, onClickRevokeConsent: () -> Unit) { + var expanded by remember { mutableStateOf(false) } + + IconButton( + onClick = { expanded = true } + ) { + Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = DpOffset(24.dp, 0.dp) + ) { + DropdownMenuItem( + onClick = { + onClickRevokeConsent() + expanded = false + }, + enabled = consentGranted + ) { + Text( + text = + stringResource(R.string.profile_revoke_consent) + ) + } + } +} + +@Composable +fun Invoices( + listState: LazyListState +) { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + state = listState + ) { + item { + InvoicesEmptyScreen() + } + } +} + +@Composable +fun LazyItemScope.InvoicesEmptyScreen() { + Column( + modifier = Modifier + .fillParentMaxSize() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painterResource(R.drawable.girl_red_oh_no), + contentDescription = null + ) + Text( + stringResource(R.string.invoices_no_invoices), + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun ConnectBottomBar(onClickConnect: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .background( + color = AppTheme.colors.primary100 + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.invoices_connect_info), + modifier = Modifier + .padding(PaddingDefaults.Medium) + .weight(1f), + style = AppTheme.typography.body2 + ) + Button( + onClick = onClickConnect, + modifier = Modifier.padding(end = PaddingDefaults.Medium) + ) { + Text(text = stringResource(R.string.invoices_connect_btn)) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt index e44686a8..ac3fa202 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt @@ -20,22 +20,22 @@ package de.gematik.ti.erp.app.prescription.detail.ui import android.content.Context import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.prescription.ui.GenerellErrorState +import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState fun deleteErrorMessage(context: Context, deleteState: PrescriptionServiceErrorState): String? = when (deleteState) { - GenerellErrorState.NetworkNotAvailable -> + GeneralErrorState.NetworkNotAvailable -> context.getString(R.string.error_message_network_not_available) - is GenerellErrorState.ServerCommunicationFailedWhileRefreshing -> + is GeneralErrorState.ServerCommunicationFailedWhileRefreshing -> context.getString(R.string.error_message_server_communication_failed).format(deleteState.code) - GenerellErrorState.FatalTruststoreState -> + GeneralErrorState.FatalTruststoreState -> context.getString(R.string.error_message_vau_error) is DeletePrescriptions.State.Error.PrescriptionWorkflowBlocked -> context.getString(R.string.logout_delete_in_progress) is DeletePrescriptions.State.Error.PrescriptionNotFound -> context.getString(R.string.prescription_not_found) - is GenerellErrorState.NoneEnrolled -> + is GeneralErrorState.NoneEnrolled -> context.getString(R.string.no_auth_enrolled) else -> null } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt index 6c6deb2e..b4acdaa0 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +34,6 @@ import androidx.navigation.NavController import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens -import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -47,7 +45,7 @@ import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter @Composable -fun ArchiveScreen(prescriptionViewModel: PrescriptionViewModel, navController: NavController, onBack: () -> Unit) { +fun ArchiveScreen(prescriptionState: PrescriptionState, navController: NavController, onBack: () -> Unit) { val listState = rememberLazyListState() AnimatedElevationScaffold( topBarTitle = stringResource(R.string.archive_screen_title), @@ -55,11 +53,7 @@ fun ArchiveScreen(prescriptionViewModel: PrescriptionViewModel, navController: N onBack = onBack, navigationMode = NavigationBarMode.Back ) { - val state by produceState(null) { - prescriptionViewModel.screenState().collect { - value = it - } - } + val state by prescriptionState.state LazyColumn( modifier = Modifier.fillMaxSize().testTag(TestTag.Prescriptions.Archive.Content), diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt index 98b6adf8..1f204289 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt @@ -32,6 +32,7 @@ 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 @@ -40,8 +41,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.core.MainViewModel import de.gematik.ti.erp.app.mainscreen.ui.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 @@ -50,13 +51,15 @@ 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 @Composable fun MlKitIntroScreen( navController: NavController, - mainViewModel: MainViewModel + settingsController: SettingsController ) { val listState = rememberLazyListState() + val scope = rememberCoroutineScope() AnimatedElevationScaffold( modifier = Modifier @@ -65,7 +68,9 @@ fun MlKitIntroScreen( bottomBar = { MlKitBottomBar( onAccept = { - mainViewModel.acceptMlKit() + scope.launch { + settingsController.acceptMlKit() + } navController.navigate(MainNavigationScreens.Camera.path()) }, onClickReadMore = { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt index 5451a1be..d68ad13d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt @@ -27,7 +27,6 @@ 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.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -50,7 +49,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow @@ -59,7 +57,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -73,7 +70,7 @@ import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData @@ -112,8 +109,8 @@ const val TWO_DAYS_LEFT = 2L @Composable fun PrescriptionScreen( navController: NavController, - prescriptionViewModel: PrescriptionViewModel, - mainScreenViewModel: MainScreenViewModel, + prescriptionState: PrescriptionState, + mainScreenController: MainScreenController, onClickAvatar: () -> Unit, onClickArchive: () -> Unit, onElevateTopBar: (Boolean) -> Unit @@ -138,11 +135,11 @@ fun PrescriptionScreen( RefreshScaffold( profileId = profileId, onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, onShowCardWall = onShowCardWall ) { onRefresh -> Prescriptions( - prescriptionViewModel = prescriptionViewModel, + prescriptionState = prescriptionState, onClickRefresh = { onRefresh(true, MutatePriority.UserInput) }, @@ -177,29 +174,23 @@ val CardPaddingModifier = Modifier @Composable private fun Prescriptions( - prescriptionViewModel: PrescriptionViewModel, + prescriptionState: PrescriptionState, navController: NavController, onClickRefresh: () -> Unit, onClickAvatar: () -> Unit, onClickArchive: () -> Unit, onElevateTopBar: (Boolean) -> Unit ) { - val state by produceState(null) { - prescriptionViewModel.screenState().collect { - value = it - } - } - - state?.let { - PrescriptionsContent( - onClickRefresh = onClickRefresh, - onClickAvatar = onClickAvatar, - state = it, - navController = navController, - onElevateTopBar = onElevateTopBar, - onClickArchive = onClickArchive - ) - } + val state by prescriptionState.state + + PrescriptionsContent( + onClickRefresh = onClickRefresh, + onClickAvatar = onClickAvatar, + state = state, + navController = navController, + onElevateTopBar = onElevateTopBar, + onClickArchive = onClickArchive + ) } private val FabPadding = 68.dp diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt index fb0bc736..d4c37377 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt @@ -26,12 +26,12 @@ interface PrescriptionServiceState interface PrescriptionServiceErrorState : PrescriptionServiceState @Stable -sealed interface GenerellErrorState : PrescriptionServiceErrorState { - object NetworkNotAvailable : GenerellErrorState - class ServerCommunicationFailedWhileRefreshing(val code: Int) : GenerellErrorState - object FatalTruststoreState : GenerellErrorState - object NoneEnrolled : GenerellErrorState - object UserNotAuthenticated : GenerellErrorState +sealed interface GeneralErrorState : PrescriptionServiceErrorState { + object NetworkNotAvailable : GeneralErrorState + class ServerCommunicationFailedWhileRefreshing(val code: Int) : GeneralErrorState + object FatalTruststoreState : GeneralErrorState + object NoneEnrolled : GeneralErrorState + object UserNotAuthenticated : GeneralErrorState } @Immutable diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionState.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionState.kt new file mode 100644 index 00000000..669d7068 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionState.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.saveable.rememberSaveable +import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData +import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.activeProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import org.kodein.di.compose.rememberInstance + +@Stable +class PrescriptionState( + prescriptionUseCase: PrescriptionUseCase, + profileId: ProfileIdentifier +) { + private val prescriptionFlow = combine( + prescriptionUseCase.scannedActiveRecipes(profileId), + prescriptionUseCase.syncedActiveRecipes(profileId) + ) { lowDetail, fullDetail -> + (lowDetail + fullDetail) + } + + private val stateFlow: Flow = + combine( + prescriptionFlow, + prescriptionUseCase.redeemedPrescriptions(profileId) + ) { prescriptions, redeemed -> + PrescriptionScreenData.State( + prescriptions = prescriptions, + redeemedPrescriptions = redeemed + ) + }.distinctUntilChanged() + + val state + @Composable + get() = stateFlow.collectAsState(PrescriptionScreenData.EmptyState) +} + +@Composable +fun rememberPrescriptionState(): PrescriptionState { + val prescriptionUseCase by rememberInstance() + val activeProfile = LocalProfileHandler.current.activeProfile + + return rememberSaveable(activeProfile.id, saver = complexAutoSaver()) { + PrescriptionState( + prescriptionUseCase = prescriptionUseCase, + profileId = activeProfile.id + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt deleted file mode 100644 index 579a4697..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.ui - -import androidx.lifecycle.viewModelScope -import de.gematik.ti.erp.app.DispatchProvider -import androidx.lifecycle.ViewModel -import de.gematik.ti.erp.app.prescription.ui.model.PrescriptionScreenData -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.activeProfile -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch - -class PrescriptionViewModel( - private val prescriptionUseCase: PrescriptionUseCase, - private val profilesUseCase: ProfilesUseCase, - private val dispatchers: DispatchProvider -) : ViewModel() { - private val timeTrigger = MutableSharedFlow() - - init { - viewModelScope.launch { - while (true) { - delay(timeMillis = 1000L * 60L) - timeTrigger.emit(Unit) - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun screenState(): Flow = - profilesUseCase.profiles.map { it.activeProfile() }.flatMapLatest { activeProfile -> - val prescriptionFlow = combine( - prescriptionUseCase.scannedActiveRecipes(activeProfile.id), - timeTrigger - .onStart { emit(Unit) } - .flatMapLatest { prescriptionUseCase.syncedActiveRecipes(activeProfile.id) } - .distinctUntilChanged() - ) { lowDetail, fullDetail -> - (lowDetail + fullDetail) - } - - combine( - prescriptionFlow, - prescriptionUseCase.redeemedPrescriptions(activeProfile.id) - ) { prescriptions, redeemed -> - // TODO: split redeemed & unredeemed - PrescriptionScreenData.State( - prescriptions = prescriptions, - redeemedPrescriptions = redeemed - ) - } - }.distinctUntilChanged().flowOn(dispatchers.Default) -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt index 91dd8a59..d4207bd3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt @@ -30,7 +30,7 @@ import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.idp.usecase.IDPConfigException import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController import de.gematik.ti.erp.app.prescription.usecase.RefreshPrescriptionUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.vau.interceptor.VauException @@ -51,7 +51,7 @@ import java.net.UnknownHostException @Stable class RefreshPrescriptionsController( private val refreshPrescriptionUseCase: RefreshPrescriptionUseCase, - private val mainScreenViewModel: MainScreenViewModel, + private val mainScreenController: MainScreenController, private val authenticator: Authenticator ) { @@ -71,14 +71,14 @@ class RefreshPrescriptionsController( ).cancellable().first() when (finalState) { - GenerellErrorState.NoneEnrolled -> { + GeneralErrorState.NoneEnrolled -> { onShowCardWall() } - GenerellErrorState.UserNotAuthenticated -> { + GeneralErrorState.UserNotAuthenticated -> { onUserNotAuthenticated() } else -> { - mainScreenViewModel.onRefresh(finalState) + mainScreenController.onRefresh(finalState) } } } @@ -100,14 +100,14 @@ class RefreshPrescriptionsController( } @Composable -fun rememberRefreshPrescriptionsController(mainScreenViewModel: MainScreenViewModel): RefreshPrescriptionsController { +fun rememberRefreshPrescriptionsController(mainScreenController: MainScreenController): RefreshPrescriptionsController { val refreshPrescriptionUseCase by rememberInstance() val authenticator = LocalAuthenticator.current return remember { RefreshPrescriptionsController( refreshPrescriptionUseCase = refreshPrescriptionUseCase, - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, authenticator = authenticator ) } @@ -149,24 +149,24 @@ fun Flow.catchAndTransformRemoteExceptions() = throwable.walkCause()?.also { emit(it) } ?: throw throwable } -private fun Throwable.walkCause(): GenerellErrorState? = +private fun Throwable.walkCause(): GeneralErrorState? = cause?.walkCause() ?: transformException() -private fun Throwable.transformException(): GenerellErrorState? = +private fun Throwable.transformException(): GeneralErrorState? = when (this) { is UserNotAuthenticatedException -> - GenerellErrorState.UserNotAuthenticated + GeneralErrorState.UserNotAuthenticated is NoneEnrolledException -> - GenerellErrorState.NoneEnrolled + GeneralErrorState.NoneEnrolled is VauException -> - GenerellErrorState.FatalTruststoreState + GeneralErrorState.FatalTruststoreState is IDPConfigException -> // TODO use other state - GenerellErrorState.FatalTruststoreState + GeneralErrorState.FatalTruststoreState is SocketTimeoutException, is UnknownHostException -> - GenerellErrorState.NetworkNotAvailable + GeneralErrorState.NetworkNotAvailable is ApiCallException -> - GenerellErrorState.ServerCommunicationFailedWhileRefreshing( + GeneralErrorState.ServerCommunicationFailedWhileRefreshing( this.response.code() ) else -> null diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt index 77af656c..5fcb8d0a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt @@ -132,7 +132,7 @@ 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.BottomSheetAction -import de.gematik.ti.erp.app.utils.compose.Spacer4 +import de.gematik.ti.erp.app.utils.compose.SpacerTiny 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.annotatedPluralsResource @@ -537,7 +537,7 @@ private fun InfoCard( private fun InfoError(text: String) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.Close, null, modifier = Modifier.size(24.dp)) - Spacer4() + SpacerTiny() Text( text, textAlign = TextAlign.Center, diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt index 573aead5..de4b1c1d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt @@ -27,4 +27,6 @@ object PrescriptionScreenData { val prescriptions: List, val redeemedPrescriptions: List ) + + val EmptyState = State(emptyList(), emptyList()) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt index 951395b8..e00d00be 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt @@ -23,17 +23,21 @@ import TokenScreen import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController +import de.gematik.ti.erp.app.pkv.ui.InvoiceInformationScreen import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +import de.gematik.ti.erp.app.settings.ui.SettingsController +import de.gematik.ti.erp.app.settings.ui.SettingStatesData import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.NavigationMode +import kotlinx.coroutines.launch object ProfileDestinations { object Profile : Route("profile") @@ -42,41 +46,51 @@ object ProfileDestinations { object PairedDevices : Route("pairedDevices") object ProfileImagePicker : Route("profileImagePicker") object ProfileImageCropper : Route("imageCropper") + object InvoiceInformation : Route("invoiceInformation") } @Composable fun EditProfileNavGraph( - state: SettingsScreen.State, + profilesState: SettingStatesData.ProfilesState, navController: NavHostController, onBack: () -> Unit, selectedProfile: ProfilesUseCaseData.Profile, - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, + mainScreenController: MainScreenController, profileSettingsViewModel: ProfileSettingsViewModel, onRemoveProfile: (newProfileName: String?) -> Unit, mainNavController: NavController ) { + val scope = rememberCoroutineScope() NavHost(navController = navController, startDestination = ProfileDestinations.Profile.route) { composable(ProfileDestinations.Profile.route) { EditProfileScreenContent( onClickToken = { navController.navigate(ProfileDestinations.Token.path()) }, onClickAuditEvents = { navController.navigate(ProfileDestinations.AuditEvents.path()) }, onClickLogIn = { - settingsViewModel.switchProfile(selectedProfile) + scope.launch { + settingsController.switchProfile(selectedProfile) + } mainNavController.navigate( MainNavigationScreens.CardWall.path(selectedProfile.id) ) }, - onClickLogout = { settingsViewModel.logout(selectedProfile) }, + onClickLogout = { + scope.launch { + settingsController.logout(selectedProfile) + } + }, onBack = onBack, - state = state, - settingsViewModel = settingsViewModel, + profilesState = profilesState, + settingsController = settingsController, profileSettingsViewModel = profileSettingsViewModel, selectedProfile = selectedProfile, onRemoveProfile = onRemoveProfile, onClickEditAvatar = { navController.navigate(ProfileDestinations.ProfileImagePicker.path()) }, onClickPairedDevices = { navController.navigate(ProfileDestinations.PairedDevices.path()) - } + }, + onClickInvoiceInformation = { navController.navigate(ProfileDestinations.InvoiceInformation.path()) } ) } @@ -114,7 +128,7 @@ fun EditProfileNavGraph( } composable(ProfileDestinations.Token.route) { - val accessToken by settingsViewModel.decryptedAccessToken(selectedProfile).collectAsState(null) + val accessToken by settingsController.decryptedAccessToken(selectedProfile).collectAsState(null) NavigationAnimation(mode = NavigationMode.Closed) { TokenScreen( @@ -128,7 +142,7 @@ fun EditProfileNavGraph( NavigationAnimation(mode = NavigationMode.Closed) { AuditEventsScreen( profileId = selectedProfile.id, - viewModel = settingsViewModel, + settingsController = settingsController, lastAuthenticated = selectedProfile.lastAuthenticated, tokenValid = selectedProfile.ssoTokenValid() ) { navController.popBackStack() } @@ -138,10 +152,24 @@ fun EditProfileNavGraph( NavigationAnimation(mode = NavigationMode.Closed) { PairedDevicesScreen( selectedProfile = selectedProfile, - settingsViewModel = settingsViewModel, + settingsController = settingsController, onBack = { navController.popBackStack() } ) } } + + composable(ProfileDestinations.InvoiceInformation.route) { + NavigationAnimation(mode = NavigationMode.Closed) { + InvoiceInformationScreen( + mainScreenController = mainScreenController, + selectedProfile = selectedProfile, + onBack = { navController.popBackStack() } + ) { + mainNavController.navigate( + MainNavigationScreens.CardWall.path(selectedProfile.id) + ) + } + } + } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt index 643b6d17..be7e713b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt @@ -105,11 +105,12 @@ import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.TestTag.Profile.OpenTokensScreenButton import de.gematik.ti.erp.app.TestTag.Profile.ProfileScreen import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.mainscreen.ui.rememberMainScreenController import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.settings.ui.ProfileNameDialog -import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +import de.gematik.ti.erp.app.settings.ui.SettingsController +import de.gematik.ti.erp.app.settings.ui.SettingStatesData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -127,22 +128,24 @@ import kotlinx.datetime.Instant @Composable fun EditProfileScreen( - state: SettingsScreen.State, + profilesState: SettingStatesData.ProfilesState, profile: ProfilesUseCaseData.Profile, - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel, onRemoveProfile: (newProfileName: String?) -> Unit, onBack: () -> Unit, mainNavController: NavController ) { val navController = rememberNavController() + val mainScreenController = rememberMainScreenController() EditProfileNavGraph( - state = state, + profilesState = profilesState, navController = navController, onBack = onBack, selectedProfile = profile, - settingsViewModel = settingsViewModel, + settingsController = settingsController, + mainScreenController = mainScreenController, profileSettingsViewModel = profileSettingsViewModel, onRemoveProfile = onRemoveProfile, mainNavController = mainNavController @@ -152,30 +155,29 @@ fun EditProfileScreen( @Composable fun EditProfileScreen( profileId: String, - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel, onBack: () -> Unit, mainNavController: NavController ) { - val state by produceState(initialValue = SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } + val profilesState by settingsController.profilesState + val scope = rememberCoroutineScope() - state.profileById(profileId)?.let { profile -> + profilesState.profileById(profileId)?.let { profile -> val selectedProfile = remember(profile) { profile } EditProfileScreen( - state = state, + profilesState = profilesState, onBack = onBack, profile = selectedProfile, - settingsViewModel = settingsViewModel, + settingsController = settingsController, profileSettingsViewModel = profileSettingsViewModel, onRemoveProfile = { - settingsViewModel.removeProfile(profile, it) - onBack() + scope.launch { + settingsController.removeProfile(profile, it) + onBack() + } }, mainNavController = mainNavController ) @@ -187,8 +189,8 @@ fun EditProfileScreen( fun EditProfileScreenContent( onBack: () -> Unit, selectedProfile: ProfilesUseCaseData.Profile, - state: SettingsScreen.State, - settingsViewModel: SettingsViewModel, + profilesState: SettingStatesData.ProfilesState, + settingsController: SettingsController, profileSettingsViewModel: ProfileSettingsViewModel, onRemoveProfile: (newProfileName: String?) -> Unit, onClickEditAvatar: () -> Unit, @@ -196,7 +198,8 @@ fun EditProfileScreenContent( onClickLogIn: () -> Unit, onClickLogout: () -> Unit, onClickAuditEvents: () -> Unit, - onClickPairedDevices: () -> Unit + onClickPairedDevices: () -> Unit, + onClickInvoiceInformation: () -> Unit ) { val listState = rememberLazyListState() val scaffoldState = rememberScaffoldState() @@ -208,7 +211,7 @@ fun EditProfileScreenContent( deleteProfileDialog( onCancel = { deleteProfileDialogVisible = false }, onClickAction = { - if (state.profiles.size == 1) { + if (profilesState.profiles.size == 1) { showAddDefaultProfileDialog = true } else { onRemoveProfile(null) @@ -244,7 +247,7 @@ fun EditProfileScreenContent( item { ProfileNameSection( profile = selectedProfile, - state = state, + profilesState = profilesState, onChangeProfileName = { profileSettingsViewModel.updateProfileName(selectedProfile.id, it) } @@ -268,7 +271,7 @@ fun EditProfileScreenContent( if (selectedProfile.insuranceInformation.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { item { - ProfileInvoiceInformation {} + ProfileInvoiceInformation { onClickInvoiceInformation() } } } @@ -282,7 +285,7 @@ fun EditProfileScreenContent( if (showAddDefaultProfileDialog) { ProfileNameDialog( - settingsViewModel = settingsViewModel, + settingsController = settingsController, wantRemoveLastProfile = true, onEdit = { showAddDefaultProfileDialog = false; onRemoveProfile(it) }, onDismissRequest = { showAddDefaultProfileDialog = false } @@ -486,7 +489,7 @@ fun SettingsMenuHeadline(text: String) { @Composable fun ProfileNameSection( profile: ProfilesUseCaseData.Profile, - state: SettingsScreen.State, + profilesState: SettingStatesData.ProfilesState, onChangeProfileName: (String) -> Unit ) { var profileName by remember(profile.name) { mutableStateOf(profile.name) } @@ -548,7 +551,7 @@ fun ProfileNameSection( profileName = name profileNameValid = isValid }, - state = state, + profilesState = profilesState, onDone = { if (profileNameValid) { onChangeProfileName(profileName) @@ -585,7 +588,7 @@ fun ProfileEditBasicTextField( textStyle: TextStyle = AppTheme.typography.h5, initialProfileName: String, onChangeProfileName: (String, Boolean) -> Unit, - state: SettingsScreen.State, + profilesState: SettingStatesData.ProfilesState, onDone: () -> Unit ) { var profileNameState by remember { @@ -613,7 +616,7 @@ fun ProfileEditBasicTextField( onChangeProfileName( name, name.trim().equals(initialProfileName, true) || - !state.containsProfileWithName(name) && name.isNotEmpty() + !profilesState.containsProfileWithName(name) && name.isNotEmpty() ) }, enabled = enabled, diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt index 7fae3155..14e18e44 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt @@ -65,7 +65,7 @@ import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +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 @@ -123,7 +123,7 @@ fun ProfileEditPairedDeviceSection( @Composable fun PairedDevicesScreen( selectedProfile: ProfilesUseCaseData.Profile, - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, onBack: () -> Unit ) { val listState = rememberLazyListState() @@ -137,7 +137,7 @@ fun PairedDevicesScreen( PairedDevices( modifier = Modifier.padding(it), selectedProfile = selectedProfile, - settingsViewModel = settingsViewModel, + settingsController = settingsController, listState = listState ) } @@ -175,7 +175,7 @@ private sealed interface DeleteState { private fun PairedDevices( modifier: Modifier, selectedProfile: ProfilesUseCaseData.Profile, - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, listState: LazyListState ) { val authenticator = LocalAuthenticator.current @@ -187,7 +187,7 @@ private fun PairedDevices( .onStart { emit(Unit) } // emit once to start the flow directly .collectLatest { state = RefreshState.Loading - settingsViewModel + settingsController .pairedDevices(selectedProfile.id) .retry(1) { throwable -> Napier.e("Couldn't get paired devices", throwable) @@ -244,7 +244,7 @@ private fun PairedDevices( onClickAction = { coroutineScope.launch { mutex.mutate { - settingsViewModel + settingsController .deletePairedDevice(selectedProfile.id, it.device) .onFailure { deleteState = DeleteState.Error diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt index cf1fa863..6b3acc79 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt @@ -173,7 +173,7 @@ fun LocalRedeemScreen( } ) } - if (codes.size > 1 || codes.first().nrOfCodes > 1) { + if (codes.size > 1 || (codes.size == 1 && codes.first().nrOfCodes > 1)) { PageIndicator( modifier = Modifier.align(Alignment.CenterHorizontally), pagerState = pagerState @@ -213,41 +213,44 @@ private fun DataMatrix( val matrix = remember(code) { createBitMatrix(code.payload) } val shape = RoundedCornerShape(16.dp) - Column( - modifier = modifier - .background(Color.White, shape) - .border(1.dp, AppTheme.colors.neutral300, shape) - .padding(PaddingDefaults.Medium) - ) { - @Suppress("MagicNumber") - Box( - modifier = Modifier - .scale(scale = if (isZoomedOut) 0.7f else 1f) - .drawDataMatrix(matrix) - .aspectRatio(1f) - .fillMaxWidth() - ) - SpacerMedium() - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - if (code.name != null) { - Text( - modifier = Modifier.weight(1f), - text = code.name, - style = AppTheme.typography.h6, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } else { - Spacer(Modifier.weight(1f)) - } + AppTheme(darkTheme = false) { + Column( + modifier = modifier + .background(AppTheme.colors.neutral000, shape) + .border(1.dp, AppTheme.colors.neutral300, shape) + .padding(PaddingDefaults.Medium) + ) { + @Suppress("MagicNumber") + Box( + modifier = Modifier + .scale(scale = if (isZoomedOut) 0.7f else 1f) + .drawDataMatrix(matrix) + .aspectRatio(1f) + .fillMaxWidth() + ) SpacerMedium() - TertiaryButton( - onClick = { onClickZoom(!isZoomedOut) } - ) { - if (isZoomedOut) { - Icon(Icons.Rounded.ZoomIn, null) + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + if (code.name != null) { + Text( + modifier = Modifier.weight(1f), + text = code.name, + style = AppTheme.typography.h6, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = AppTheme.colors.neutral999 + ) } else { - Icon(Icons.Rounded.ZoomOut, null) + Spacer(Modifier.weight(1f)) + } + SpacerMedium() + TertiaryButton( + onClick = { onClickZoom(!isZoomedOut) } + ) { + if (isZoomedOut) { + Icon(Icons.Rounded.ZoomIn, null) + } else { + Icon(Icons.Rounded.ZoomOut, null) + } } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt index f19856e3..30bd6870 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt @@ -23,7 +23,7 @@ import androidx.compose.runtime.getValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenViewModel +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController 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.ui.rememberPharmacyOrderState @@ -33,7 +33,7 @@ import de.gematik.ti.erp.app.utils.compose.navigationModeState @Composable fun RedeemNavigation( - mainScreenViewModel: MainScreenViewModel, + mainScreenController: MainScreenController, onFinish: () -> Unit ) { val orderState = rememberPharmacyOrderState() @@ -108,7 +108,7 @@ fun RedeemNavigation( } composable(RedeemNavigation.PharmacySearch.route) { PharmacyNavigation( - mainScreenViewModel = mainScreenViewModel, + mainScreenController = mainScreenController, isNestedNavigation = true, orderState = orderState, onBack = { diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt index e5100c9d..40b514d4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt @@ -19,6 +19,7 @@ package de.gematik.ti.erp.app.redeem.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.shareIn import org.kodein.di.compose.rememberInstance +@Stable class RedeemController( scope: CoroutineScope, val profileId: ProfileIdentifier, diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt index 9c78d14f..b9f19438 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt @@ -21,9 +21,9 @@ package de.gematik.ti.erp.app.redeem.ui.model import de.gematik.ti.erp.app.Route class RedeemNavigation { - object HowToRedeem : Route("HowToRedeem") - object PrescriptionSelection : Route("PrescriptionSelection") - object LocalRedeem : Route("LocalRedeem") - object OnlineRedeem : Route("OnlineRedeem") - object PharmacySearch : Route("PharmacySearch") + object HowToRedeem : Route("redeem_how_to") + object PrescriptionSelection : Route("redeem_prescription_selection") + object LocalRedeem : Route("redeem_local") + object OnlineRedeem : Route("redeem_online") + object PharmacySearch : Route("redeem_pharmacy_search") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt index 331a70b2..afb9d600 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt @@ -24,28 +24,25 @@ import androidx.compose.material.icons.rounded.ZoomIn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.settings.ui.SettingsScreen -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.utils.compose.AcceptDialog import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.LabeledSwitch import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import kotlinx.coroutines.launch @Composable -fun AccessibilitySettingsScreen(settingsViewModel: SettingsViewModel, onBack: () -> Unit) { - val state by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } - +fun AccessibilitySettingsScreen(settingsController: SettingsController, onBack: () -> Unit) { + val zoomState by settingsController.zoomState + val screenshotState by settingsController.screenShotState + val scope = rememberCoroutineScope() val listState = rememberLazyListState() var showAllowScreenShotsAlert by remember { mutableStateOf(false) } @@ -62,18 +59,22 @@ fun AccessibilitySettingsScreen(settingsViewModel: SettingsViewModel, onBack: () ) { item { SpacerMedium() - ZoomSection(zoomChecked = state.zoomEnabled) { zoomEnabled -> + ZoomSection(zoomChecked = zoomState.zoomEnabled) { zoomEnabled -> when (zoomEnabled) { - true -> settingsViewModel.onEnableZoom() - false -> settingsViewModel.onDisableZoom() + true -> scope.launch { + settingsController.onEnableZoom() + } + false -> scope.launch { + settingsController.onDisableZoom() + } } } } item { AllowScreenShotsSection( - state.screenshotsAllowed + screenshotState.screenshotsAllowed ) { screenShotsAllowed -> - settingsViewModel.onSwitchAllowScreenshots(screenShotsAllowed) + settingsController.onSwitchAllowScreenshots(screenShotsAllowed) showAllowScreenShotsAlert = true } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt index be0f2a7c..8571a5a3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.semantics.semantics import androidx.compose.foundation.layout.navigationBarsPadding import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar -import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -74,7 +73,6 @@ fun AllowBiometryScreen( ) { if (showBiometricPrompt) { BiometricPrompt( - authenticationMethod = SettingsData.AuthenticationMode.DeviceSecurity, title = stringResource(R.string.auth_prompt_headline), description = "", negativeButton = stringResource(R.string.auth_prompt_cancel), diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt index 4d59c643..69241155 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt @@ -42,7 +42,7 @@ import androidx.paging.compose.itemsIndexed import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.settings.ui.SettingsViewModel +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 @@ -59,13 +59,13 @@ import java.time.LocalDateTime @Composable fun AuditEventsScreen( profileId: ProfileIdentifier, - viewModel: SettingsViewModel, + settingsController: SettingsController, lastAuthenticated: Instant?, tokenValid: Boolean, onBack: () -> Unit ) { val header = stringResource(R.string.autitEvents_headline) - val auditEventPagingFlow = remember(profileId) { viewModel.loadAuditEventsForProfile(profileId) } + val auditEventPagingFlow = remember(profileId) { settingsController.loadAuditEventsForProfile(profileId) } val pagingItems = auditEventPagingFlow.collectAsLazyPagingItems() val listState = rememberLazyListState() diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt index 1a0611a8..5155c9ea 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt @@ -38,7 +38,6 @@ 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.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -60,16 +59,11 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium @Composable fun DeviceSecuritySettingsScreen( - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, onBack: () -> Unit, onClickProtectionMode: (SettingsData.AuthenticationMode) -> Unit - ) { - val authenticationMode by produceState(SettingsScreen.defaultState.authenticationMode) { - settingsViewModel.screenState().collect { - value = it.authenticationMode - } - } + val authenticationModeState by settingsController.authenticationModeState val listState = rememberLazyListState() @@ -77,7 +71,6 @@ fun DeviceSecuritySettingsScreen( if (showBiometricPrompt) { BiometricPrompt( - authenticationMethod = SettingsData.AuthenticationMode.DeviceSecurity, title = stringResource(R.string.auth_prompt_headline), description = "", negativeButton = stringResource(R.string.auth_prompt_cancel), @@ -110,7 +103,8 @@ fun DeviceSecuritySettingsScreen( SpacerMedium() AuthenticationModeCard( Icons.Outlined.Fingerprint, - checked = authenticationMode == SettingsData.AuthenticationMode.DeviceSecurity, + checked = authenticationModeState.authenticationMode is + SettingsData.AuthenticationMode.DeviceSecurity, headline = stringResource(R.string.settings_appprotection_device_security_header), info = stringResource(R.string.settings_appprotection_device_security_info), deviceSecurity = true @@ -121,7 +115,7 @@ fun DeviceSecuritySettingsScreen( item { AuthenticationModeCard( Icons.Outlined.Security, - checked = authenticationMode is SettingsData.AuthenticationMode.Password, + checked = authenticationModeState.authenticationMode is SettingsData.AuthenticationMode.Password, headline = stringResource(R.string.settings_appprotection_mode_password_headline), info = stringResource(R.string.settings_appprotection_mode_password_info) ) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt index 28e75497..42733537 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt @@ -93,7 +93,7 @@ import java.util.Locale private const val MinimalPasswordScore = 2 @Composable -fun SecureAppWithPassword(navController: NavController, viewModel: SettingsViewModel) { +fun SecureAppWithPassword(navController: NavController, settingsController: SettingsController) { var password by remember { mutableStateOf("") } var repeatedPassword by remember { mutableStateOf("") } var passwordScore by remember { mutableStateOf(0) } @@ -113,7 +113,7 @@ fun SecureAppWithPassword(navController: NavController, viewModel: SettingsViewM Button( onClick = { coroutineScope.launch { - viewModel.onSelectPasswordAsAuthenticationMode(password) + settingsController.onSelectPasswordAsAuthenticationMode(password) navController.popBackStack() } }, @@ -180,7 +180,7 @@ fun SecureAppWithPassword(navController: NavController, viewModel: SettingsViewM ) ) { coroutineScope.launch { - viewModel.onSelectPasswordAsAuthenticationMode(password) + settingsController.onSelectPasswordAsAuthenticationMode(password) navController.popBackStack() } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt index c9295dc1..54852a78 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt @@ -33,7 +33,6 @@ 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.produceState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -52,15 +51,11 @@ import de.gematik.ti.erp.app.utils.compose.provideWebIntent @Composable fun ProductImprovementSettingsScreen( - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, onAllowAnalytics: (Boolean) -> Unit, onBack: () -> Unit ) { - val state by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } + val analyticsState by settingsController.analyticsState val listState = rememberLazyListState() @@ -77,7 +72,7 @@ fun ProductImprovementSettingsScreen( item { SpacerMedium() AnalyticsSection( - state.analyticsAllowed + analyticsState.analyticsAllowed ) { allow -> onAllowAnalytics(allow) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt new file mode 100644 index 00000000..fbba7119 --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.settings.ui + +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.core.content.edit +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import androidx.paging.PagingData +import de.gematik.ti.erp.app.ScreenshotsAllowed +import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.di.ApplicationPreferencesTag +import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.settings.model.SettingsData +import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking +import org.kodein.di.compose.rememberInstance + +@Suppress("TooManyFunctions") +class SettingsController( + private val settingsUseCase: SettingsUseCase, + private val profilesUseCase: ProfilesUseCase, + private val profilesWithPairedDevicesUseCase: ProfilesWithPairedDevicesUseCase, + private val analytics: Analytics, + private val appPrefs: SharedPreferences +) { + + private var screenshotsAllowed = + MutableStateFlow(appPrefs.getBoolean(ScreenshotsAllowed, false)) + + private var screenShotFlow = screenshotsAllowed.map { SettingStatesData.ScreenshotState(it) } + + val screenShotState + @Composable + get() = screenShotFlow.collectAsState(SettingStatesData.defaultScreenshotState) + + private val analyticsFlow = analytics.analyticsAllowed.map { SettingStatesData.AnalyticsState(it) } + + val analyticsState + @Composable + get() = analyticsFlow.collectAsState(SettingStatesData.defaultAnalyticsState) + + private val authenticationModeFlow = settingsUseCase.authenticationMode.map { + SettingStatesData.AuthenticationModeState( + it + ) + } + + val authenticationModeState + @Composable + get() = authenticationModeFlow.collectAsState(SettingStatesData.defaultAuthenticationState) + + private val zoomFlow = settingsUseCase.general.map { SettingStatesData.ZoomState(it.zoomEnabled) } + + val zoomState + @Composable + get() = zoomFlow.collectAsState(SettingStatesData.defaultZoomState) + + private val profilesFlow = profilesUseCase.profiles.map { SettingStatesData.ProfilesState(it) } + + val profilesState + @Composable + get() = profilesFlow.collectAsState(SettingStatesData.defaultProfilesState) + + fun pairedDevices(profileId: ProfileIdentifier) = + profilesWithPairedDevicesUseCase.pairedDevices(profileId) + + // tag::DeletePairedDevicesViewModel[] + suspend fun deletePairedDevice(profileId: ProfileIdentifier, device: ProfilesUseCaseData.PairedDevice) = + profilesWithPairedDevicesUseCase.deletePairedDevices(profileId, device) + + // end::DeletePairedDevicesViewModel[] + fun decryptedAccessToken(profile: ProfilesUseCaseData.Profile) = + profilesUseCase.decryptedAccessToken(profile.id) + + suspend fun onSelectDeviceSecurityAuthenticationMode() { + settingsUseCase.saveAuthenticationMode( + SettingsData.AuthenticationMode.DeviceSecurity + ) + } + + suspend fun onSelectPasswordAsAuthenticationMode(password: String) { + settingsUseCase.saveAuthenticationMode(SettingsData.AuthenticationMode.Password(password = password)) + } + + fun onSwitchAllowScreenshots(allowScreenshots: Boolean) { + appPrefs.edit { + putBoolean(ScreenshotsAllowed, allowScreenshots) + } + screenshotsAllowed.value = allowScreenshots + } + + suspend fun onEnableZoom() { + settingsUseCase.saveZoomPreference(true) + } + + suspend fun onDisableZoom() { + settingsUseCase.saveZoomPreference(false) + } + + fun onTrackingAllowed() { + analytics.allowTracking() + } + + fun onTrackingDisallowed() { + analytics.disallowTracking() + } + + suspend fun logout(profile: ProfilesUseCaseData.Profile) { + profilesUseCase.logout(profile) + } + + suspend fun addProfile(profileName: String) { + profilesUseCase.addProfile(profileName, activate = true) + } + + suspend fun removeProfile(profile: ProfilesUseCaseData.Profile, newProfileName: String?) { + if (newProfileName != null) { + profilesUseCase.removeAndSaveProfile(profile, newProfileName) + } else { + profilesUseCase.removeProfile(profile) + } + } + + suspend fun switchProfile(profile: ProfilesUseCaseData.Profile) { + profilesUseCase.switchActiveProfile(profile) + } + + fun loadAuditEventsForProfile(profileId: ProfileIdentifier): Flow> = + profilesUseCase.auditEvents(profileId) + + suspend fun onboardingSucceeded( + authenticationMode: SettingsData.AuthenticationMode, + defaultProfileName: String, + allowTracking: Boolean + ) { + settingsUseCase.onboardingSucceeded( + authenticationMode = authenticationMode, + defaultProfileName = defaultProfileName + ) + if (allowTracking) { + onTrackingAllowed() + } else { + onTrackingDisallowed() + } + } + + val authenticationMethod = settingsUseCase.authenticationMode + 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) { + insecureDevicePromptShown = true + it + } else { + false + } + } + + suspend fun onAcceptInsecureDevice() { + settingsUseCase.acceptInsecureDevice() + } + + suspend fun acceptMlKit() { + settingsUseCase.acceptMlKit() + } + + suspend fun acceptUpdatedDataTerms() { + settingsUseCase.acceptUpdatedDataTerms() + } + + 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 profilesUseCase by rememberInstance() + val profilesWithPairedDevicesUseCase by rememberInstance() + val analytics by rememberInstance() + val appPrefs by rememberInstance(ApplicationPreferencesTag) + + return remember { + SettingsController( + settingsUseCase, + profilesUseCase, + profilesWithPairedDevicesUseCase, + analytics, + appPrefs + ) + } +} + +object SettingStatesData { + + @Immutable + data class AnalyticsState( + val analyticsAllowed: Boolean + ) + + val defaultAnalyticsState = AnalyticsState(analyticsAllowed = false) + + @Immutable + data class AuthenticationModeState( + val authenticationMode: SettingsData.AuthenticationMode + ) + + val defaultAuthenticationState = AuthenticationModeState(SettingsData.AuthenticationMode.Unspecified) + + @Immutable + data class ZoomState( + val zoomEnabled: Boolean + ) + + val defaultZoomState = ZoomState(zoomEnabled = false) + + @Immutable + data class ScreenshotState( + val screenshotsAllowed: Boolean + ) + + // `gemSpec_eRp_FdV A_20203` default settings does not allow screenshots + val defaultScreenshotState = ScreenshotState(screenshotsAllowed = false) + + @Immutable + data class ProfilesState( + val profiles: List + ) { + fun activeProfile() = profiles.find { it.active }!! + fun profileById(profileId: String) = profiles.find { it.id == profileId } + fun containsProfileWithName(name: String) = profiles.any { + it.name.equals(name.trim(), true) + } + } + + val defaultProfilesState = ProfilesState( + profiles = listOf() + ) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt index e7f284f0..854d0e6c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt @@ -21,6 +21,7 @@ package de.gematik.ti.erp.app.settings.ui import AccessibilitySettingsScreen import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.navigation.NavController @@ -34,6 +35,7 @@ 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.createToastShort +import kotlinx.coroutines.launch object SettingsNavigationScreens { object Settings : Route("Settings") @@ -49,8 +51,9 @@ fun SettingsNavGraph( settingsNavController: NavHostController, navigationMode: NavigationMode, mainNavController: NavController, - settingsViewModel: SettingsViewModel + settingsController: SettingsController ) { + val scope = rememberCoroutineScope() NavHost( settingsNavController, startDestination = SettingsNavigationScreens.Settings.path() @@ -60,13 +63,13 @@ fun SettingsNavGraph( SettingsScreenWithScaffold( mainNavController = mainNavController, navController = settingsNavController, - settingsViewModel = settingsViewModel + settingsController = settingsController ) } } composable(SettingsNavigationScreens.AccessibilitySettings.route) { AccessibilitySettingsScreen( - settingsViewModel = settingsViewModel, + settingsController = settingsController, onBack = { settingsNavController.popBackStack() } ) } @@ -75,10 +78,10 @@ fun SettingsNavGraph( val disAllowAnalyticsToast = stringResource(R.string.settings_tracking_disallow_info) ProductImprovementSettingsScreen( - settingsViewModel = settingsViewModel, + settingsController = settingsController, onAllowAnalytics = { if (!it) { - settingsViewModel.onTrackingDisallowed() + settingsController.onTrackingDisallowed() createToastShort(context, disAllowAnalyticsToast) } else { mainNavController.navigate(MainNavigationScreens.AllowAnalytics.path()) @@ -90,13 +93,16 @@ fun SettingsNavGraph( composable(SettingsNavigationScreens.DeviceSecuritySettings.route) { DeviceSecuritySettingsScreen( - settingsViewModel = settingsViewModel, + settingsController = settingsController, onBack = { settingsNavController.popBackStack() } ) { when (it) { is SettingsData.AuthenticationMode.Password -> mainNavController.navigate(MainNavigationScreens.Password.path()) - else -> settingsViewModel.onSelectDeviceSecurityAuthenticationMode() + else -> + scope.launch { + settingsController.onSelectDeviceSecurityAuthenticationMode() + } } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt index e3274452..92d4ee3d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt @@ -64,7 +64,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -98,7 +97,7 @@ 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.Spacer4 +import de.gematik.ti.erp.app.utils.compose.SpacerTiny 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 @@ -112,7 +111,7 @@ import java.util.Locale @Composable fun SettingsScreen( mainNavController: NavController, - settingsViewModel: SettingsViewModel + settingsController: SettingsController ) { val settingsNavController = rememberNavController() @@ -122,7 +121,7 @@ fun SettingsScreen( settingsNavController = settingsNavController, navigationMode = navigationMode, mainNavController = mainNavController, - settingsViewModel = settingsViewModel + settingsController = settingsController ) } @@ -130,13 +129,9 @@ fun SettingsScreen( fun SettingsScreenWithScaffold( mainNavController: NavController, navController: NavController, - settingsViewModel: SettingsViewModel + settingsController: SettingsController ) { - val state by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } + val profilesState by settingsController.profilesState val listState = rememberLazyListState() @@ -157,7 +152,7 @@ fun SettingsScreenWithScaffold( } } item { - ProfileSection(state, mainNavController) + ProfileSection(profilesState, mainNavController) SettingsDivider() } item { @@ -252,10 +247,10 @@ private fun SettingsDivider() = @Composable private fun ProfileSection( - state: SettingsScreen.State, + profilesState: SettingStatesData.ProfilesState, navController: NavController ) { - val profiles = state.profiles + val profiles = profilesState.profiles Column { Text( @@ -318,16 +313,12 @@ private fun ProfileCard( @Composable fun ProfileNameDialog( initialProfileName: String = "", - settingsViewModel: SettingsViewModel, + settingsController: SettingsController, wantRemoveLastProfile: Boolean = false, onEdit: (text: String) -> Unit, onDismissRequest: () -> Unit ) { - val settingsScreenState by produceState(SettingsScreen.defaultState) { - settingsViewModel.screenState().collect { - value = it - } - } + val profilesState by settingsController.profilesState var textValue by remember { mutableStateOf(initialProfileName ?: "") } var duplicated by remember { mutableStateOf(false) } @@ -370,7 +361,7 @@ fun ProfileNameDialog( val name = sanitizeProfileName(it.trimStart()) textValue = name duplicated = textValue.trim() != initialProfileName && - settingsScreenState.containsProfileWithName(textValue) && + profilesState.containsProfileWithName(textValue) && !wantRemoveLastProfile }, keyboardOptions = KeyboardOptions( @@ -605,12 +596,12 @@ private fun AboutSection(modifier: Modifier) { ) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Rounded.PhoneAndroid, null, modifier = Modifier.size(16.dp)) - Spacer4() + SpacerTiny() Text( stringResource(R.string.about_version, BuildConfig.VERSION_NAME) ) } - Spacer4() + SpacerTiny() Text( stringResource(R.string.about_buildhash, BuildKonfig.GIT_HASH) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt b/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt deleted file mode 100644 index d157d5cc..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsViewModel.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.settings.ui - -import android.content.SharedPreferences -import androidx.compose.runtime.Immutable -import androidx.core.content.edit -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.ScreenshotsAllowed -import androidx.lifecycle.ViewModel -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.protocol.model.AuditEventData -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase -import de.gematik.ti.erp.app.analytics.Analytics -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch - -object SettingsScreen { - @Immutable - data class State( - val analyticsAllowed: Boolean, - val authenticationMode: SettingsData.AuthenticationMode, - val zoomEnabled: Boolean, - val screenshotsAllowed: Boolean, - val profiles: List - ) { - fun activeProfile() = profiles.find { it.active }!! - fun profileById(profileId: String) = profiles.find { it.id == profileId } - fun containsProfileWithName(name: String) = profiles.any { - it.name.equals(name.trim(), true) - } - } - - val defaultState = State( - analyticsAllowed = false, - authenticationMode = SettingsData.AuthenticationMode.Unspecified, - zoomEnabled = false, - // `gemSpec_eRp_FdV A_20203` default settings does not allow screenshots - screenshotsAllowed = false, - profiles = listOf() - ) -} - -class SettingsViewModel( - private val settingsUseCase: SettingsUseCase, - private val profilesUseCase: ProfilesUseCase, - private val profilesWithPairedDevicesUseCase: ProfilesWithPairedDevicesUseCase, - private val analytics: Analytics, - private val appPrefs: SharedPreferences, - private val dispatchers: DispatchProvider -) : ViewModel() { - - private var screenshotsAllowed = - MutableStateFlow(appPrefs.getBoolean(ScreenshotsAllowed, false)) - - fun screenState() = combine( - analytics.analyticsAllowed, - settingsUseCase.general, - settingsUseCase.authenticationMode, - screenshotsAllowed, - profilesUseCase.profiles - ) { analyticsAllowed, settings, authenticationMode, screenshotsAllowed, profiles -> - SettingsScreen.State( - zoomEnabled = settings.zoomEnabled, - analyticsAllowed = analyticsAllowed, - authenticationMode = authenticationMode, - screenshotsAllowed = screenshotsAllowed, - profiles = profiles - ) - }.flowOn(dispatchers.Default) - - fun pairedDevices(profileId: ProfileIdentifier) = - profilesWithPairedDevicesUseCase.pairedDevices(profileId) - - // tag::DeletePairedDevicesViewModel[] - suspend fun deletePairedDevice(profileId: ProfileIdentifier, device: ProfilesUseCaseData.PairedDevice) = - profilesWithPairedDevicesUseCase.deletePairedDevices(profileId, device) - - // end::DeletePairedDevicesViewModel[] - fun decryptedAccessToken(profile: ProfilesUseCaseData.Profile) = - profilesUseCase.decryptedAccessToken(profile.id) - - fun onSelectDeviceSecurityAuthenticationMode() = - viewModelScope.launch(Dispatchers.IO) { - settingsUseCase.saveAuthenticationMode( - SettingsData.AuthenticationMode.DeviceSecurity - ) - } - - fun onSelectPasswordAsAuthenticationMode(password: String) = - viewModelScope.launch(Dispatchers.IO) { - settingsUseCase.saveAuthenticationMode(SettingsData.AuthenticationMode.Password(password = password)) - } - - fun onSwitchAllowScreenshots(allowScreenshots: Boolean) { - appPrefs.edit { - putBoolean(ScreenshotsAllowed, allowScreenshots) - } - screenshotsAllowed.value = allowScreenshots - } - - fun onEnableZoom() { - viewModelScope.launch { - settingsUseCase.saveZoomPreference(true) - } - } - - fun onDisableZoom() { - viewModelScope.launch { - settingsUseCase.saveZoomPreference(false) - } - } - - fun onTrackingAllowed() { - analytics.allowTracking() - } - - fun onTrackingDisallowed() { - analytics.disallowTracking() - } - - fun logout(profile: ProfilesUseCaseData.Profile) { - viewModelScope.launch { - profilesUseCase.logout(profile) - } - } - - fun addProfile(profileName: String) { - viewModelScope.launch { - profilesUseCase.addProfile(profileName, activate = true) - } - } - - fun removeProfile(profile: ProfilesUseCaseData.Profile, newProfileName: String?) { - viewModelScope.launch { - if (newProfileName != null) { - profilesUseCase.removeAndSaveProfile(profile, newProfileName) - } else { - profilesUseCase.removeProfile(profile) - } - } - } - - fun switchProfile(profile: ProfilesUseCaseData.Profile) { - viewModelScope.launch { - profilesUseCase.switchActiveProfile(profile) - } - } - - fun loadAuditEventsForProfile(profileId: ProfileIdentifier): Flow> = - profilesUseCase.auditEvents(profileId) - - suspend fun onboardingSucceeded( - authenticationMode: SettingsData.AuthenticationMode, - defaultProfileName: String, - allowTracking: Boolean - ) { - settingsUseCase.onboardingSucceeded( - authenticationMode = authenticationMode, - defaultProfileName = defaultProfileName - ) - if (allowTracking) { - onTrackingAllowed() - } else { - onTrackingDisallowed() - } - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt index 38a2e199..30e31238 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt @@ -91,7 +91,6 @@ class AuthenticationUseCase( when (lifecycle) { Lifecycle.Created -> { this@AuthenticationUseCase.authRequired.value = when (authenticationMode) { - SettingsData.AuthenticationMode.None, SettingsData.AuthenticationMode.Unspecified -> false else -> true } @@ -102,7 +101,6 @@ class AuthenticationUseCase( } Lifecycle.Running -> { when (authenticationMode) { - SettingsData.AuthenticationMode.None, SettingsData.AuthenticationMode.Unspecified -> emit(AuthenticationModeAndMethod.Authenticated) else -> if (authRequired) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt index 1b126ca6..ef895ea7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt @@ -27,14 +27,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.utils.compose.createToastShort // tag::BiometricPromptAndBestSecureOption[] @Composable fun BiometricPrompt( - authenticationMethod: SettingsData.AuthenticationMode, title: String, description: String, negativeButton: String, @@ -87,39 +85,17 @@ fun BiometricPrompt( val promptInfo = remember { val secureOption = bestSecureOption(biometricManager) - if (authenticationMethod == SettingsData.AuthenticationMode.DeviceCredentials) { - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .setAllowedAuthenticators( - BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) - .build() - } else if (authenticationMethod == SettingsData.AuthenticationMode.Biometrics) { - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .setNegativeButtonText(negativeButton) - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG - ) - .setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_WEAK - ) - .build() - } else { - BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setDescription(description) - .apply { - if ((secureOption and BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { - setNegativeButtonText(negativeButton) - } - }.setAllowedAuthenticators( - secureOption - ) - .build() - } + BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setDescription(description) + .apply { + if ((secureOption and BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { + setNegativeButtonText(negativeButton) + } + }.setAllowedAuthenticators( + secureOption + ) + .build() } val biometricPrompt = remember { BiometricPrompt(activity, executor, callback) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt index 942341a4..9dc170ac 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt @@ -191,7 +191,6 @@ fun UserAuthenticationScreen() { ) else -> BiometricPrompt( - authenticationMethod = state.authenticationMethod, title = stringResource(R.string.auth_prompt_headline), description = "", negativeButton = stringResource(R.string.auth_prompt_cancel), diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt index 9b6bf487..9cc500a7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt @@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding @@ -136,62 +135,6 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.Date -@Composable -fun Spacer32() = - Spacer(modifier = Modifier.size(32.dp)) - -@Composable -fun Spacer24() = - Spacer(modifier = Modifier.size(24.dp)) - -@Composable -fun Spacer16() = - Spacer(modifier = Modifier.size(16.dp)) - -@Composable -fun Spacer8() = - Spacer(modifier = Modifier.size(8.dp)) - -@Composable -fun Spacer4() = - Spacer(modifier = Modifier.size(4.dp)) - -@Composable -fun Spacer48() = - Spacer(modifier = Modifier.size(48.dp)) - -@Composable -fun SpacerLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.Large)) - -@Composable -fun SpacerXLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.XLarge)) - -@Composable -fun SpacerXXLarge() = - Spacer(modifier = Modifier.size(PaddingDefaults.XXLarge)) - -@Composable -fun SpacerMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.Medium)) - -@Composable -fun SpacerShortMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.ShortMedium)) - -@Composable -fun SpacerXXLargeMedium() = - Spacer(modifier = Modifier.size(PaddingDefaults.XXLargeMedium)) - -@Composable -fun SpacerSmall() = - Spacer(modifier = Modifier.size(PaddingDefaults.Small)) - -@Composable -fun SpacerTiny() = - Spacer(modifier = Modifier.size(PaddingDefaults.Tiny)) - @Composable fun LargeButton( onClick: () -> Unit, diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Spacer.kt b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Spacer.kt new file mode 100644 index 00000000..a775f03e --- /dev/null +++ b/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Spacer.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import de.gematik.ti.erp.app.theme.PaddingDefaults + +@Composable +fun SpacerTiny() = + Spacer(modifier = Modifier.size(PaddingDefaults.Tiny)) + +@Composable +fun SpacerSmall() = + Spacer(modifier = Modifier.size(PaddingDefaults.Small)) + +@Composable +fun SpacerMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.Medium)) + +@Composable +fun SpacerShortMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.ShortMedium)) + +@Composable +fun SpacerLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.Large)) + +@Composable +fun SpacerXLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.XLarge)) + +@Composable +fun SpacerXXLarge() = + Spacer(modifier = Modifier.size(PaddingDefaults.XXLarge)) + +@Composable +fun SpacerXXLargeMedium() = + Spacer(modifier = Modifier.size(PaddingDefaults.XXLargeMedium)) diff --git a/android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt index 278b56a6..7c1a7642 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt +++ b/android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt @@ -193,7 +193,8 @@ fun createWebViewClient(colors: Colors, typo: Typography) = object : WebViewClie } override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - return if (request.url.scheme == "https" && request.url.host != "localhost") { + val isAllowedScheme = request.url.scheme == "https" || request.url.scheme == "mailto" + return if (isAllowedScheme && request.url.host != "localhost") { view.context.startActivity(Intent(Intent.ACTION_VIEW, request.url)) true } else { diff --git a/android/src/main/res/raw/nfc_positions.json b/android/src/main/res/raw/nfc_positions.json index c3059a02..d1817729 100644 --- a/android/src/main/res/raw/nfc_positions.json +++ b/android/src/main/res/raw/nfc_positions.json @@ -1,5 +1,6 @@ [ { + "manufacturer": "Huawei", "marketingName": "HONOR 10", "modelNames": [ "COL-AL00", @@ -7,24 +8,30 @@ "COL-L29", "COL-TL10" ], - "x0": 0.07851239669421484, - "y0": 0.011210762331838564, - "x1": 0.5165289256198347, - "y1": 0.09865470852017937 + "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" ], - "x0": 0.265625, - "y0": 0.014084507042253521, - "x1": 0.7135416666666667, - "y1": 0.15023474178403756 + "nfcPos": { + "x0": 0.265625, + "y0": 0.014084507042253521, + "x1": 0.7135416666666667, + "y1": 0.15023474178403756 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR 9", "modelNames": [ "STF-AL00", @@ -33,33 +40,42 @@ "STF-L09S", "STF-TL10" ], - "x0": 0.0, - "y0": 0.0, - "x1": 0.4530386740331491, - "y1": 0.08735632183908046 + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.4530386740331491, + "y1": 0.08735632183908046 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR Magic 2", "modelNames": [ "TNY-AL00", "TNY-TL00" ], - "x0": 0.0, - "y0": 0.0, - "x1": 0.6772486772486772, - "y1": 0.10304449648711944 + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.6772486772486772, + "y1": 0.10304449648711944 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR Note10", "modelNames": [ "RVL-AL09" ], - "x0": 0.17803030303030298, - "y0": 0.0044943820224719105, - "x1": 0.7992424242424243, - "y1": 0.056179775280898875 + "nfcPos": { + "x0": 0.17803030303030298, + "y0": 0.0044943820224719105, + "x1": 0.7992424242424243, + "y1": 0.056179775280898875 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR Play", "modelNames": [ "COR-AL00", @@ -67,45 +83,57 @@ "COR-L29", "COR-TL10" ], - "x0": 0.042857142857142816, - "y0": 0.020179372197309416, - "x1": 0.6761904761904762, - "y1": 0.12556053811659193 + "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" ], - "x0": 0.07851239669421484, - "y0": 0.011210762331838564, - "x1": 0.5165289256198347, - "y1": 0.09865470852017937 + "nfcPos": { + "x0": 0.07851239669421484, + "y0": 0.011210762331838564, + "x1": 0.5165289256198347, + "y1": 0.09865470852017937 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR V20", "modelNames": [ "PCT-TL10" ], - "x0": 0.3121693121693122, - "y0": 0.07621247113163972, - "x1": 0.5873015873015873, - "y1": 0.19630484988452657 + "nfcPos": { + "x0": 0.3121693121693122, + "y0": 0.07621247113163972, + "x1": 0.5873015873015873, + "y1": 0.19630484988452657 + } }, { + "manufacturer": "Huawei", "marketingName": "HONOR V9", "modelNames": [ "DUK-AL20", "DUK-TL30" ], - "x0": 0.0, - "y0": 0.0, - "x1": 0.4530386740331491, - "y1": 0.08735632183908046 + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.4530386740331491, + "y1": 0.08735632183908046 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI Mate 10-Serie", "modelNames": [ "ALP-AL00", @@ -122,12 +150,15 @@ "RNE-L21", "RNE-L23" ], - "x0": 0.1701030927835051, - "y0": 0.0, - "x1": 0.7731958762886598, - "y1": 0.12413793103448276 + "nfcPos": { + "x0": 0.1701030927835051, + "y0": 0.0, + "x1": 0.7731958762886598, + "y1": 0.12413793103448276 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI Mate 20-Serie", "modelNames": [ "HMA-L09", @@ -155,28 +186,67 @@ "SNE-LX2", "SNE-LX3" ], - "x0": 0.2328042328042328, - "y0": 0.10161662817551963, - "x1": 0.7671957671957672, - "y1": 0.2678983833718245 + "nfcPos": { + "x0": 0.2328042328042328, + "y0": 0.10161662817551963, + "x1": 0.7671957671957672, + "y1": 0.2678983833718245 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI Mate 30-Serie", - "modelNames": [], - "x0": 0.12903225806451613, - "y0": 0.020833333333333332, - "x1": 0.8333333333333334, - "y1": 0.3055555555555556 - }, - { + "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": [], - "x0": 0.08860759493670889, - "y0": 0.012587412587412588, - "x1": 0.9113924050632911, - "y1": 0.35664335664335667 - }, - { + "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", @@ -188,42 +258,57 @@ "LON-AL00", "LON-L29" ], - "x0": 0.17431192660550454, - "y0": 0.0022935779816513763, - "x1": 0.8027522935779816, - "y1": 0.06422018348623854 + "nfcPos": { + "x0": 0.17431192660550454, + "y0": 0.0022935779816513763, + "x1": 0.8027522935779816, + "y1": 0.06422018348623854 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI Mate RS", "modelNames": [ "NEO-AL00", "NEO-L29" ], - "x0": 0.13440860215053763, - "y0": 0.02947845804988662, - "x1": 0.8763440860215054, - "y1": 0.1836734693877551 + "nfcPos": { + "x0": 0.13440860215053763, + "y0": 0.02947845804988662, + "x1": 0.8763440860215054, + "y1": 0.1836734693877551 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI Mate X-Serie", - "modelNames": [], - "x0": 0.023400936037441533, - "y0": 0.0, - "x1": 0.37597503900156004, - "y1": 0.04741980474198047 + "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" ], - "x0": 0.07106598984771573, - "y0": 0.0022988505747126436, - "x1": 0.5076142131979695, - "y1": 0.12413793103448276 + "nfcPos": { + "x0": 0.07106598984771573, + "y0": 0.0022988505747126436, + "x1": 0.5076142131979695, + "y1": 0.12413793103448276 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI nova 3-Serie", "modelNames": [ "INE-LX1", @@ -244,12 +329,15 @@ "INE-LX2", "INE-TL00" ], - "x0": 0.0, - "y0": 0.0, - "x1": 0.7679558011049724, - "y1": 0.0979020979020979 + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.7679558011049724, + "y1": 0.0979020979020979 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI P10-Serie", "modelNames": [ "VTR-AL00", @@ -267,12 +355,15 @@ "WAS-LX2J", "WAS-LX3" ], - "x0": 0.1707317073170732, - "y0": 0.0, - "x1": 0.5951219512195122, - "y1": 0.07209302325581396 + "nfcPos": { + "x0": 0.1707317073170732, + "y0": 0.0, + "x1": 0.5951219512195122, + "y1": 0.07209302325581396 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI P20-Serie", "modelNames": [ "ANE-LX2J", @@ -296,12 +387,15 @@ "CLT-L09", "CLT-L29" ], - "x0": 0.0871794871794872, - "y0": 0.02546296296296296, - "x1": 0.7128205128205128, - "y1": 0.2708333333333333 + "nfcPos": { + "x0": 0.0871794871794872, + "y0": 0.02546296296296296, + "x1": 0.7128205128205128, + "y1": 0.2708333333333333 + } }, { + "manufacturer": "Huawei", "marketingName": "HUAWEI P30-Serie", "modelNames": [ "ELE-AL00", @@ -334,25 +428,49 @@ "VOG-TL00", "MAR-LX2J" ], - "x0": 0.07853403141361259, - "y0": 0.020737327188940093, - "x1": 0.6073298429319371, - "y1": 0.2557603686635945 + "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" - ], - "x0": 0.032573289902280145, - "y0": 0.04495912806539509, - "x1": 0.501628664495114, - "y1": 0.3201634877384196 - }, - { + "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", @@ -363,12 +481,15 @@ "SM-A326W", "SM-S326DL" ], - "x0": 0.0699300699300699, - "y0": 0.29354838709677417, - "x1": 0.8951048951048951, - "y1": 0.603225806451613 + "nfcPos": { + "x0": 0.0699300699300699, + "y0": 0.29354838709677417, + "x1": 0.8951048951048951, + "y1": 0.603225806451613 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A42 5G", "modelNames": [ "SM-A4260", @@ -378,23 +499,29 @@ "SM-A426U1", "SM-S426DL" ], - "x0": 0.049645390070921946, - "y0": 0.26129032258064516, - "x1": 0.9290780141843972, - "y1": 0.6903225806451613 + "nfcPos": { + "x0": 0.049645390070921946, + "y0": 0.26129032258064516, + "x1": 0.9290780141843972, + "y1": 0.6903225806451613 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A50s", "modelNames": [ "SM-A5070", "SM-A507FN" ], - "x0": 0.217687074829932, - "y0": 0.1064516129032258, - "x1": 0.7006802721088435, - "y1": 0.3064516129032258 + "nfcPos": { + "x0": 0.217687074829932, + "y0": 0.1064516129032258, + "x1": 0.7006802721088435, + "y1": 0.3064516129032258 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A51", "modelNames": [ "SM-A515F", @@ -403,12 +530,15 @@ "SM-A515W", "SM-S515DL" ], - "x0": 0.08450704225352113, - "y0": 0.08108108108108109, - "x1": 0.6056338028169015, - "y1": 0.2905405405405405 + "nfcPos": { + "x0": 0.08450704225352113, + "y0": 0.08108108108108109, + "x1": 0.6056338028169015, + "y1": 0.2905405405405405 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A52 5G", "modelNames": [ "SC-53B", @@ -419,23 +549,29 @@ "SM-A526U1", "SM-A526W" ], - "x0": 0.03597122302158273, - "y0": 0.0, - "x1": 0.5611510791366907, - "y1": 0.28378378378378377 + "nfcPos": { + "x0": 0.03597122302158273, + "y0": 0.0, + "x1": 0.5611510791366907, + "y1": 0.28378378378378377 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A60", "modelNames": [ "SM-A6060", "SM-A606Y" ], - "x0": 0.1842105263157895, - "y0": 0.13306451612903225, - "x1": 0.7456140350877193, - "y1": 0.3225806451612903 + "nfcPos": { + "x0": 0.1842105263157895, + "y0": 0.13306451612903225, + "x1": 0.7456140350877193, + "y1": 0.3225806451612903 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A70", "modelNames": [ "SM-A7050", @@ -447,31 +583,43 @@ "SM-A705W", "SM-A705YN" ], - "x0": 0.2206896551724138, - "y0": 0.12903225806451613, - "x1": 0.7655172413793103, - "y1": 0.3064516129032258 + "nfcPos": { + "x0": 0.2206896551724138, + "y0": 0.12903225806451613, + "x1": 0.7655172413793103, + "y1": 0.3064516129032258 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A71", "modelNames": [ "SM-A715F", "SM-A715W" ], - "x0": 0.07638888888888884, - "y0": 0.050335570469798654, - "x1": 0.5972222222222222, - "y1": 0.2684563758389262 + "nfcPos": { + "x0": 0.07638888888888884, + "y0": 0.050335570469798654, + "x1": 0.5972222222222222, + "y1": 0.2684563758389262 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A8+", - "modelNames": [], - "x0": 0.1095890410958904, - "y0": 0.3076923076923077, - "x1": 0.8561643835616438, - "y1": 0.5737179487179487 + "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", @@ -481,71 +629,100 @@ "SM-A800I", "SM-A800IZ", "SM-A8000", - "SM-A800X" - ], - "x0": 0.19424460431654678, - "y0": 0.06752411575562701, - "x1": 0.7769784172661871, - "y1": 0.21221864951768488 - }, - { + "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" ], - "x0": 0.13013698630136983, - "y0": 0.18387096774193548, - "x1": 0.8356164383561644, - "y1": 0.47419354838709676 + "nfcPos": { + "x0": 0.13013698630136983, + "y0": 0.18387096774193548, + "x1": 0.8356164383561644, + "y1": 0.47419354838709676 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy A8s", "modelNames": [ "SM-G887F", "SM-G8870" ], - "x0": 0.25, - "y0": 0.10240963855421686, - "x1": 0.7714285714285715, - "y1": 0.3072289156626506 + "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" ], - "x0": 0.04402515723270439, - "y0": 0.03115264797507788, - "x1": 0.9559748427672956, - "y1": 0.4143302180685358 + "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" ], - "x0": 0.23076923076923073, - "y0": 0.08012820512820513, - "x1": 0.6474358974358974, - "y1": 0.2403846153846154 + "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" ], - "x0": 0.27338129496402874, - "y0": 0.0641025641025641, - "x1": 0.7122302158273381, - "y1": 0.21794871794871795 + "nfcPos": { + "x0": 0.27338129496402874, + "y0": 0.0641025641025641, + "x1": 0.7122302158273381, + "y1": 0.21794871794871795 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy C9 Pro", "modelNames": [ "SM-C900F", @@ -554,12 +731,15 @@ "SM-C9008", "SM-C900X" ], - "x0": 0.22857142857142854, - "y0": 0.07333333333333333, - "x1": 0.6928571428571428, - "y1": 0.20333333333333334 + "nfcPos": { + "x0": 0.22857142857142854, + "y0": 0.07333333333333333, + "x1": 0.6928571428571428, + "y1": 0.20333333333333334 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Fold", "modelNames": [ "SCV44", @@ -569,22 +749,28 @@ "SM-F900U1", "SM-F900W" ], - "x0": 0.15315315315315314, - "y0": 0.38387096774193546, - "x1": 0.9099099099099099, - "y1": 0.6387096774193548 + "nfcPos": { + "x0": 0.15315315315315314, + "y0": 0.38387096774193546, + "x1": 0.9099099099099099, + "y1": 0.6387096774193548 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note10 Lite", "modelNames": [ "SM-N770F" ], - "x0": 0.10416666666666663, - "y0": 0.08389261744966443, - "x1": 0.5555555555555556, - "y1": 0.2953020134228188 + "nfcPos": { + "x0": 0.10416666666666663, + "y0": 0.08389261744966443, + "x1": 0.5555555555555556, + "y1": 0.2953020134228188 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note10+", "modelNames": [ "SC-01M", @@ -596,12 +782,15 @@ "SM-N975W", "SM-N975F" ], - "x0": 0.13157894736842102, - "y0": 0.06129032258064516, - "x1": 0.7171052631578947, - "y1": 0.35161290322580646 + "nfcPos": { + "x0": 0.09547738693467334, + "y0": 0.06129032258064516, + "x1": 0.542713567839196, + "y1": 0.35161290322580646 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note10", "modelNames": [ "SM-N970F", @@ -610,12 +799,15 @@ "SM-N970U1", "SM-N970W" ], - "x0": 0.11538461538461542, - "y0": 0.06129032258064516, - "x1": 0.6923076923076923, - "y1": 0.3419354838709677 + "nfcPos": { + "x0": 0.08374384236453203, + "y0": 0.06129032258064516, + "x1": 0.5270935960591133, + "y1": 0.3419354838709677 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note20 5G", "modelNames": [ "SM-N9810", @@ -625,12 +817,15 @@ "SM-N981W", "SM-N981B" ], - "x0": 0.10516252390057357, - "y0": 0.4105263157894737, - "x1": 0.9082217973231358, - "y1": 0.7140350877192982 + "nfcPos": { + "x0": 0.10516252390057357, + "y0": 0.4105263157894737, + "x1": 0.9082217973231358, + "y1": 0.7140350877192982 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note20 Ultra 5G", "modelNames": [ "SC-53A", @@ -642,12 +837,15 @@ "SM-N986W", "SM-N986B" ], - "x0": 0.06691449814126393, - "y0": 0.34509466437177283, - "x1": 0.9219330855018587, - "y1": 0.6858864027538726 + "nfcPos": { + "x0": 0.06691449814126393, + "y0": 0.34509466437177283, + "x1": 0.9219330855018587, + "y1": 0.6858864027538726 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note5", "modelNames": [ "SM-N9208", @@ -671,12 +869,15 @@ "SM-N920R4", "SM-N920V" ], - "x0": 0.09219858156028371, - "y0": 0.4180064308681672, - "x1": 0.9787234042553191, - "y1": 0.77491961414791 + "nfcPos": { + "x0": 0.09219858156028371, + "y0": 0.4180064308681672, + "x1": 0.9787234042553191, + "y1": 0.77491961414791 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note8", "modelNames": [ "SC-01K", @@ -690,12 +891,15 @@ "SM-N950W", "SM-N950U1" ], - "x0": 0.18055555555555558, - "y0": 0.24666666666666667, - "x1": 0.8402777777777778, - "y1": 0.6266666666666667 + "nfcPos": { + "x0": 0.18055555555555558, + "y0": 0.24666666666666667, + "x1": 0.8402777777777778, + "y1": 0.6266666666666667 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Note9", "modelNames": [ "SC-01L", @@ -707,12 +911,15 @@ "SM-N960U", "SM-N960U1" ], - "x0": 0.23239436619718312, - "y0": 0.3389261744966443, - "x1": 0.823943661971831, - "y1": 0.5906040268456376 + "nfcPos": { + "x0": 0.23239436619718312, + "y0": 0.3389261744966443, + "x1": 0.823943661971831, + "y1": 0.5906040268456376 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S10+", "modelNames": [ "SC-04L", @@ -725,12 +932,15 @@ "SM-G975U1", "SM-G975W" ], - "x0": 0.1806451612903226, - "y0": 0.3383233532934132, - "x1": 0.8129032258064516, - "y1": 0.6347305389221557 + "nfcPos": { + "x0": 0.1806451612903226, + "y0": 0.3383233532934132, + "x1": 0.8129032258064516, + "y1": 0.6347305389221557 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S10", "modelNames": [ "SC-03L", @@ -744,12 +954,15 @@ "SM-G973U1", "SM-G973W" ], - "x0": 0.05031446540880502, - "y0": 0.2433234421364985, - "x1": 0.8050314465408805, - "y1": 0.6468842729970327 + "nfcPos": { + "x0": 0.05031446540880502, + "y0": 0.2433234421364985, + "x1": 0.8050314465408805, + "y1": 0.6468842729970327 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S10e", "modelNames": [ "SM-G970F", @@ -760,51 +973,66 @@ "SM-G970U1", "SM-G970W" ], - "x0": 0.20370370370370372, - "y0": 0.322884012539185, - "x1": 0.8024691358024691, - "y1": 0.5987460815047022 + "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" ], - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S20 Ultra", "modelNames": [], - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S20+", "modelNames": [ "SM-G985F" ], - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S20", "modelNames": [ "SM-G980F" ], - "x0": 0.0680628272251309, - "y0": 0.36764705882352944, - "x1": 0.9267015706806283, - "y1": 0.7271241830065359 + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S21 5G", "modelNames": [ "SC-51B", @@ -816,27 +1044,37 @@ "SM-G991B", "SM-G991N" ], - "x0": 0.04929577464788737, - "y0": 0.46308724832214765, - "x1": 0.9436619718309859, - "y1": 0.7651006711409396 + "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" ], - "x0": 0.08904109589041098, - "y0": 0.28619528619528617, - "x1": 0.9178082191780822, - "y1": 0.6531986531986532 + "nfcPos": { + "x0": 0.08904109589041098, + "y0": 0.28619528619528617, + "x1": 0.9178082191780822, + "y1": 0.6531986531986532 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S21 Ultra 5G", "modelNames": [ "SC-52B", @@ -847,12 +1085,15 @@ "SM-G998B", "SM-G998N" ], - "x0": 0.02877697841726623, - "y0": 0.40604026845637586, - "x1": 0.9568345323741008, - "y1": 0.7550335570469798 + "nfcPos": { + "x0": 0.02877697841726623, + "y0": 0.40604026845637586, + "x1": 0.9568345323741008, + "y1": 0.7550335570469798 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S21+ 5G", "modelNames": [ "SCG10", @@ -862,12 +1103,15 @@ "SM-G996B", "SM-G996N" ], - "x0": 0.035211267605633756, - "y0": 0.39932885906040266, - "x1": 0.971830985915493, - "y1": 0.7550335570469798 + "nfcPos": { + "x0": 0.035211267605633756, + "y0": 0.39932885906040266, + "x1": 0.971830985915493, + "y1": 0.7550335570469798 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S22 Ultra", "modelNames": [ "SC-52C", @@ -880,12 +1124,15 @@ "SM-S908W", "SM-S908B" ], - "x0": 0.007633587786259555, - "y0": 0.34838709677419355, - "x1": 1.0, - "y1": 0.7741935483870968 + "nfcPos": { + "x0": 0.007633587786259555, + "y0": 0.34838709677419355, + "x1": 1.0, + "y1": 0.7741935483870968 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S22+", "modelNames": [ "SM-S9060", @@ -896,12 +1143,15 @@ "SM-S906W", "SM-S906B" ], - "x0": 0.05405405405405406, - "y0": 0.3258064516129032, - "x1": 0.9594594594594594, - "y1": 0.7548387096774194 + "nfcPos": { + "x0": 0.05405405405405406, + "y0": 0.3258064516129032, + "x1": 0.9594594594594594, + "y1": 0.7548387096774194 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S22", "modelNames": [ "SC-51C", @@ -914,24 +1164,30 @@ "SM-S901W", "SM-S901B" ], - "x0": 0.05921052631578949, - "y0": 0.35161290322580646, - "x1": 0.9144736842105263, - "y1": 0.7580645161290323 + "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" ], - "x0": 0.27922077922077926, - "y0": 0.36012861736334406, - "x1": 0.7272727272727273, - "y1": 0.7234726688102894 + "nfcPos": { + "x0": 0.27922077922077926, + "y0": 0.36012861736334406, + "x1": 0.7272727272727273, + "y1": 0.7234726688102894 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S7 edge", "modelNames": [ "SM-G935F", @@ -939,12 +1195,15 @@ "SM-G9350", "SM-G935U" ], - "x0": 0.09999999999999998, - "y0": 0.255663430420712, - "x1": 0.9, - "y1": 0.627831715210356 + "nfcPos": { + "x0": 0.09999999999999998, + "y0": 0.255663430420712, + "x1": 0.9, + "y1": 0.627831715210356 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S7", "modelNames": [ "SM-G930F", @@ -968,12 +1227,15 @@ "SM-G930R4", "SM-G930V" ], - "x0": 0.06578947368421051, - "y0": 0.26282051282051283, - "x1": 0.9539473684210527, - "y1": 0.6217948717948718 + "nfcPos": { + "x0": 0.06578947368421051, + "y0": 0.26282051282051283, + "x1": 0.9539473684210527, + "y1": 0.6217948717948718 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S8+", "modelNames": [ "SC-03J", @@ -985,12 +1247,15 @@ "SM-G955U", "SM-G955U1" ], - "x0": 0.15602836879432624, - "y0": 0.3054662379421222, - "x1": 0.8723404255319149, - "y1": 0.6334405144694534 + "nfcPos": { + "x0": 0.15602836879432624, + "y0": 0.3054662379421222, + "x1": 0.8723404255319149, + "y1": 0.6334405144694534 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S8", "modelNames": [ "SC-02J", @@ -1003,12 +1268,15 @@ "SM-G950U", "SM-G950U1" ], - "x0": 0.1448275862068965, - "y0": 0.36538461538461536, - "x1": 0.8551724137931034, - "y1": 0.6955128205128205 + "nfcPos": { + "x0": 0.1448275862068965, + "y0": 0.36538461538461536, + "x1": 0.8551724137931034, + "y1": 0.6955128205128205 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S9+", "modelNames": [ "SC-03K", @@ -1020,12 +1288,15 @@ "SM-G965U", "SM-G965U1" ], - "x0": 0.11564625850340138, - "y0": 0.38782051282051283, - "x1": 0.8707482993197279, - "y1": 0.7275641025641025 + "nfcPos": { + "x0": 0.11564625850340138, + "y0": 0.38782051282051283, + "x1": 0.8707482993197279, + "y1": 0.7275641025641025 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy S9", "modelNames": [ "SC-02K", @@ -1038,12 +1309,15 @@ "SM-G960U", "SM-G960U1" ], - "x0": 0.12328767123287676, - "y0": 0.3557692307692308, - "x1": 0.863013698630137, - "y1": 0.6826923076923077 + "nfcPos": { + "x0": 0.12328767123287676, + "y0": 0.3557692307692308, + "x1": 0.863013698630137, + "y1": 0.6826923076923077 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Z Flip 5G", "modelNames": [ "SCG04", @@ -1054,12 +1328,15 @@ "SM-F707U1", "SM-F707W" ], - "x0": 0.12745098039215685, - "y0": 0.6365979381443299, - "x1": 0.8333333333333334, - "y1": 0.884020618556701 + "nfcPos": { + "x0": 0.12745098039215685, + "y0": 0.6365979381443299, + "x1": 0.8333333333333334, + "y1": 0.884020618556701 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Z Flip LTE", "modelNames": [ "SCV47", @@ -1070,12 +1347,15 @@ "SM-F700U1", "SM-F700W" ], - "x0": 0.19565217391304346, - "y0": 0.6806451612903226, - "x1": 0.8043478260869565, - "y1": 0.9 + "nfcPos": { + "x0": 0.19565217391304346, + "y0": 0.6806451612903226, + "x1": 0.8043478260869565, + "y1": 0.9 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Z Flip3 5G", "modelNames": [ "SC-54B", @@ -1087,12 +1367,36 @@ "SM-F711U1", "SM-F711W" ], - "x0": 0.08088235294117652, - "y0": 0.5973154362416108, - "x1": 0.9264705882352942, - "y1": 0.912751677852349 + "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", @@ -1103,12 +1407,15 @@ "SM-F916U1", "SM-F916W" ], - "x0": 0.12959381044487428, - "y0": 0.32319078947368424, - "x1": 0.9226305609284333, - "y1": 0.6077302631578947 + "nfcPos": { + "x0": 0.12959381044487428, + "y0": 0.32319078947368424, + "x1": 0.9226305609284333, + "y1": 0.6077302631578947 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Z Fold3 5G", "modelNames": [ "SC-55B", @@ -1120,29 +1427,15 @@ "SM-F926U1", "SM-F926W" ], - "x0": 0.10526315789473684, - "y0": 0.4129032258064516, - "x1": 0.9473684210526316, - "y1": 0.7516129032258064 - }, - { - "marketingName": "Samsung Galaxy Z Flip4 5G", - "modelNames": [ - "SC-55C", - "SCG16", - "SM-F9360", - "SM-F936B", - "SM-F936N", - "SM-F936U", - "SM-F936U1", - "SM-F936W" - ], - "x0": 0.06617647058823528, - "y0": 0.5709677419354838, - "x1": 0.9264705882352942, - "y1": 0.8774193548387097 + "nfcPos": { + "x0": 0.10526315789473684, + "y0": 0.4129032258064516, + "x1": 0.9473684210526316, + "y1": 0.7516129032258064 + } }, { + "manufacturer": "Samsung", "marketingName": "Samsung Galaxy Z Fold4 5G", "modelNames": [ "SC-55C", @@ -1154,137 +1447,193 @@ "SM-F936U1", "SM-F936W" ], - "x0": 0.10447761194029848, - "y0": 0.45806451612903226, - "x1": 0.9328358208955224, - "y1": 0.7967741935483871 + "nfcPos": { + "x0": 0.10447761194029848, + "y0": 0.45806451612903226, + "x1": 0.9328358208955224, + "y1": 0.7967741935483871 + } }, { + "manufacturer": "Google", "marketingName": "Pixel (2016)", "modelNames": [ "Pixel" ], - "x0": 0.415929203539823, - "y0": 0.1091703056768559, - "x1": 0.5752212389380531, - "y1": 0.18777292576419213 + "nfcPos": { + "x0": 0.5182481751824817, + "y0": 0.0, + "x1": 0.6496350364963503, + "y1": 0.011673151750972763 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 2 (2017)", "modelNames": [ "Pixel 2" ], - "x0": 0.33884297520661155, - "y0": 0.0622568093385214, - "x1": 0.487603305785124, - "y1": 0.13229571984435798 + "nfcPos": { + "x0": 0.33884297520661155, + "y0": 0.0622568093385214, + "x1": 0.487603305785124, + "y1": 0.13229571984435798 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 3 (2018)", "modelNames": [ "Pixel 3" ], - "x0": 0.17098445595854928, - "y0": 0.12224938875305623, - "x1": 0.7305699481865284, - "y1": 0.3863080684596577 + "nfcPos": { + "x0": 0.17098445595854928, + "y0": 0.12224938875305623, + "x1": 0.7305699481865284, + "y1": 0.3863080684596577 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 3a (2019)", "modelNames": [ "Pixel 3a" ], - "x0": 0.2195121951219512, - "y0": 0.14788732394366197, - "x1": 0.7463414634146341, - "y1": 0.4014084507042254 + "nfcPos": { + "x0": 0.2195121951219512, + "y0": 0.14788732394366197, + "x1": 0.7463414634146341, + "y1": 0.4014084507042254 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 4 (2019)", "modelNames": [ "Pixel 4" ], - "x0": 0.10188679245283017, - "y0": 0.15845070422535212, - "x1": 0.41132075471698115, - "y1": 0.3028169014084507 + "nfcPos": { + "x0": 0.10188679245283017, + "y0": 0.15845070422535212, + "x1": 0.41132075471698115, + "y1": 0.3028169014084507 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 4a (2020)", "modelNames": [ "Pixel 4a" ], - "x0": 0.4957983193277311, - "y0": 0.39096267190569745, - "x1": 0.6428571428571428, - "y1": 0.45972495088408644 + "nfcPos": { + "x0": 0.4957983193277311, + "y0": 0.39096267190569745, + "x1": 0.6428571428571428, + "y1": 0.45972495088408644 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 4a (5G)", "modelNames": [ "Pixel 4a (5G)" ], - "x0": 0.44339622641509435, - "y0": 0.3858093126385809, - "x1": 0.5849056603773585, - "y1": 0.4523281596452328 + "nfcPos": { + "x0": 0.44339622641509435, + "y0": 0.3858093126385809, + "x1": 0.5849056603773585, + "y1": 0.4523281596452328 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 5", "modelNames": [ "Pixel 5" ], - "x0": 0.4416243654822335, - "y0": 0.34988179669030733, - "x1": 0.5939086294416244, - "y1": 0.42080378250591016 + "nfcPos": { + "x0": 0.4416243654822335, + "y0": 0.34988179669030733, + "x1": 0.5939086294416244, + "y1": 0.42080378250591016 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 5a (5G)", - "modelNames": [], - "x0": 0.44339622641509435, - "y0": 0.3858093126385809, - "x1": 0.5849056603773585, - "y1": 0.4523281596452328 + "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" ], - "x0": 0.43621399176954734, - "y0": 0.540952380952381, - "x1": 0.5720164609053497, - "y1": 0.6038095238095238 + "nfcPos": { + "x0": 0.43434343434343436, + "y0": 0.5373831775700935, + "x1": 0.5808080808080809, + "y1": 0.6051401869158879 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 6", "modelNames": [ "Pixel 6" ], - "x0": 0.4565217391304348, - "y0": 0.5666666666666667, - "x1": 0.6, - "y1": 0.6313725490196078 + "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" ], - "x0": 0.40888888888888886, - "y0": 0.33583489681050654, - "x1": 0.5955555555555556, - "y1": 0.4146341463414634 + "nfcPos": { + "x0": 0.4097222222222222, + "y0": 0.2602291325695581, + "x1": 0.5902777777777778, + "y1": 0.3453355155482815 + } }, { + "manufacturer": "Google", "marketingName": "Pixel 7", "modelNames": [ "Pixel 7" ], - "x0": 0.40909090909090906, - "y0": 0.26143790849673204, - "x1": 0.6, - "y1": 0.35294117647058826 + "nfcPos": { + "x0": 0.40909090909090906, + "y0": 0.26143790849673204, + "x1": 0.6, + "y1": 0.35294117647058826 + } } ] \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index dad99aed..0c54f944 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -827,4 +827,15 @@ Schließen Abrechnungen Abrechnungen anzeigen + Abrechnungen + Um Abrechnungen zu empfangen, müssen Sie mit dem Server verbunden sein. + Verbinden + Keine Abrechnungen + Deaktivieren + Abbrechen + Funktion deaktivieren + Hierdurch werden alle Abrechnungen von diesem Gerät und vom Server gelöscht. + Abrechnungen empfangen + Ihre Abrechnungen werden zusätzlich auf dem Rezeptserver gespeichert. + Empfangen diff --git a/android/src/main/res/xml/file_paths.xml b/android/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..568a0f74 --- /dev/null +++ b/android/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt b/android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt index 91005dd7..5d70873c 100644 --- a/android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt +++ b/android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt @@ -82,6 +82,9 @@ class EndpointHelper( fun getErpApiKey(): String = BuildKonfig.ERP_API_KEY + fun getIdpScope(): String = + BuildKonfig.IDP_DEFAULT_SCOPE + fun getPharmacyApiKey(): String = BuildKonfig.PHARMACY_API_KEY diff --git a/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt b/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt new file mode 100644 index 00000000..84543c90 --- /dev/null +++ b/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.model + +import de.gematik.ti.erp.app.consent.usecase.consent +import de.gematik.ti.erp.app.fhir.model.json +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedString +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConsentMapperTest { + + private val expectedInsuranceId = "X123456789" + + @Test + fun createConsent() { + val consent = createConsent(expectedInsuranceId) + + val profileString = consent.contained("meta") + .contained("profile") + .containedString() + + val insuranceId = consent.contained("patient") + .contained("identifier").contained("value").containedString() + + assertEquals("https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent|1.0", profileString) + assertEquals(expectedInsuranceId, insuranceId) + } + + @Test + fun extractConsentBundle() { + val consent = json.parseToJsonElement(consent) + val consentType = extractConsent(consent) + assertEquals(ConsentType.Charge, consentType) + } + + @Test + fun `extractConsentBundle - should return null`() { + val consent = json.parseToJsonElement("""{}""") + val consentType = extractConsent(consent) + assertEquals(null, consentType) + } +} diff --git a/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt b/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt new file mode 100644 index 00000000..0f620ee4 --- /dev/null +++ b/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.usecase + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.fhir.model.json +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.spyk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Rule +import retrofit2.Response +import java.net.HttpURLConnection +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +val consent = """{ + "id": "4af9d0b8-7d90-4606-ae3d-12a45a148ff7", + "type": "searchset", + "timestamp": "2023-02-06T08:55:38.043+00:00", + "resourceType": "Bundle", + "total": 0, + "entry": [ + { + "fullUrl": "https://erp-dev.zentral.erp.splitdns.ti-dienste.de/Consent/CHARGCONS-X764228532", + "resource": { + "resourceType": "Consent", + "id": "CHARGCONS-X764228532", + "meta": { + "profile": [ + "https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent|1.0" + ] + }, + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "patient-privacy", + "display": "Privacy Consent" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "https://gematik.de/fhir/erpchrg/CodeSystem/GEM_ERPCHRG_CS_ConsentType", + "code": "CHARGCONS", + "display": "Consent for saving electronic charge item" + } + ] + } + ], + "patient": { + "identifier": { + "system": "http://fhir.de/sid/pkv/kvid-10", + "value": "X764228532" + } + }, + "dateTime": "2023-02-03T13:19:04.642+00:00", + "policyRule": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "OPTIN" + } + ] + } + }, + "search": { + "mode": "match" + } + } + ] +} +""".trimIndent() + +class ConsentUseCaseTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + @MockK + private lateinit var consentRepository: ConsentRepository + + @MockK + private lateinit var consentUseCase: ConsentUseCase + + @BeforeTest + fun setup() { + MockKAnnotations.init(this) + consentUseCase = spyk( + ConsentUseCase(consentRepository) + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `get charge consent - consent granted`() { + val consent = json.parseToJsonElement(consent) + coEvery { consentRepository.getConsent("0") } returns Result.success(consent) + runTest { + val consentGranted = consentUseCase.getChargeConsent("0") + assertEquals(true, consentGranted.getOrThrow()) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `get charge consent - success consent not granted`() { + val consent = json.parseToJsonElement( + """{}""" + ) + coEvery { consentRepository.getConsent("0") } returns Result.success(consent) + runTest { + val consentGranted = consentUseCase.getChargeConsent("0") + assertEquals(false, consentGranted.getOrThrow()) + } + } + + @Test(expected = ApiCallException::class) + fun `get charge consent - error conflict should return consent granted `() { + coEvery { consentRepository.getConsent("0") } returns Result.failure( + ApiCallException( + "", + Response.error(HttpURLConnection.HTTP_CONFLICT, "".toResponseBody()) + ) + ) + runTest { + val consentGranted = consentUseCase.getChargeConsent("0") + assertEquals(true, consentGranted.getOrThrow()) + } + } + + @Test + fun `revoke charge consent - error not found should return consent already revoked `() { + runTest { + coEvery { consentRepository.deleteChargeConsent("0") } returns Result.failure( + ApiCallException( + "", + Response.error(HttpURLConnection.HTTP_NOT_FOUND, "".toResponseBody()) + ) + ) + val result = consentUseCase.deleteChargeConsent("0") + assertTrue(result.isFailure) + } + } + + @Test + fun `revoke charge consent - success `() { + runTest { + coEvery { consentRepository.deleteChargeConsent("0") } returns Result.success(Unit) + val result = consentUseCase.deleteChargeConsent("0") + assertTrue(result.isSuccess) + } + } +} diff --git a/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt b/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt deleted file mode 100644 index 06c5be48..00000000 --- a/android/src/test/java/de/gematik/ti/erp/app/core/MainViewModelTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.core - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.attestation.usecase.IntegrityUseCase -import de.gematik.ti.erp.app.settings.model.SettingsData -import de.gematik.ti.erp.app.settings.model.SettingsData.AppVersion -import de.gematik.ti.erp.app.settings.model.SettingsData.AuthenticationMode -import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class MainViewModelTest { - @get:Rule - val coroutineRule = CoroutineTestRule() - - private lateinit var viewModel: MainViewModel - - @MockK - private lateinit var settingsUseCase: SettingsUseCase - - @MockK - private lateinit var integrityUseCase: IntegrityUseCase - - @Before - fun setup() { - MockKAnnotations.init(this) - every { integrityUseCase.runIntegrityAttestation() } returns flow { emit(true) } - every { settingsUseCase.general } returns flowOf( - SettingsData.General( - latestAppVersion = AppVersion(code = 1, name = "Test"), - onboardingShownIn = null, - zoomEnabled = false, - userHasAcceptedInsecureDevice = false, - authenticationFails = 0, - welcomeDrawerShown = false, - mainScreenTooltipsShown = false, - mlKitAccepted = false - ) - ) - every { settingsUseCase.authenticationMode } returns flowOf(AuthenticationMode.Unspecified) - } - - @Test - fun `test showInsecureDevicePrompt - only show once`() = runTest { - every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(true) - every { settingsUseCase.showOnboarding } returns flowOf(false) - every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) - every { settingsUseCase.showMainScreenTooltip } returns flowOf(false) - - viewModel = MainViewModel(integrityUseCase, settingsUseCase) - - assertEquals(true, viewModel.showInsecureDevicePrompt.first()) - assertEquals(false, viewModel.showInsecureDevicePrompt.first()) - } - - @Test - fun `test showInsecureDevicePrompt - device is secure`() = runTest { - every { settingsUseCase.showInsecureDevicePrompt } returns flowOf(false) - every { settingsUseCase.showOnboarding } returns flowOf(false) - every { settingsUseCase.showWelcomeDrawer } returns flowOf(false) - every { settingsUseCase.showMainScreenTooltip } returns flowOf(false) - - viewModel = MainViewModel(integrityUseCase, settingsUseCase) - - assertEquals(false, viewModel.showInsecureDevicePrompt.first()) - assertEquals(false, viewModel.showInsecureDevicePrompt.first()) - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 15e6fd1b..25dcf032 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,9 +6,9 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask plugins { // reports versions of dependencies // e.g. `gradle dependencyUpdates` - id("com.github.ben-manes.versions") version "0.42.0" + id("com.github.ben-manes.versions") version "0.45.0" - id("org.owasp.dependencycheck") version "7.1.0.1" apply false + id("org.owasp.dependencycheck") version "8.0.2" apply false // generates licence report id("com.jaredsburrows.license") version "0.8.90" apply false @@ -16,13 +16,13 @@ plugins { kotlin("multiplatform") version "1.7.20" apply false kotlin("plugin.serialization") version "1.7.20" apply false id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false - id("io.realm.kotlin") version "1.4.0" apply false + id("io.realm.kotlin") version "1.6.1" apply false id("org.jetbrains.kotlin.android") version "1.7.20" apply false id("com.android.application") version "7.3.1" apply false id("com.android.library") version "7.3.1" apply false - id("org.jetbrains.compose") version "1.2.1" apply false - id("com.codingfeline.buildkonfig") version "0.11.0" apply false - id("io.gitlab.arturbosch.detekt") version "1.20.0" + id("org.jetbrains.compose") version "1.3.0" apply false + id("com.codingfeline.buildkonfig") version "0.13.3" apply false + id("io.gitlab.arturbosch.detekt") version "1.22.0" } val ktlintMain by configurations.creating diff --git a/common/build.gradle.kts b/common/build.gradle.kts index feeaa6b4..90e36b96 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -200,19 +200,18 @@ kotlin { } android { - buildToolsVersion = "33.0.0" - compileSdk = 31 + buildToolsVersion = "33.0.1" + compileSdk = 33 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = 24 - targetSdk = 30 + targetSdk = 33 } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } namespace = "de.gematik.ti.erp.lib" - // namespace = "de.gematik.ti.erp.lib" } enum class Platforms { @@ -240,15 +239,6 @@ buildkonfig { buildConfigField(STRING, "DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE", DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) buildConfigField(STRING, "DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY", DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY) buildConfigField(STRING, "BUILD_FLAVOR", project.property("buildkonfig.flavor") as String) - buildConfigField( - STRING, - "IDP_DEFAULT_SCOPE", - if (project.property("buildkonfig.flavor").toString().contains("rudev", true)) { - "e-rezept-dev openid" - } else { - "e-rezept openid" - } - ) } fun defaultConfigs( @@ -292,6 +282,8 @@ buildkonfig { buildConfigField(STRING, "APP_TRUST_ANCHOR_BASE64_PU", APP_TRUST_ANCHOR_BASE64) buildConfigField(STRING, "APP_TRUST_ANCHOR_BASE64_TU", APP_TRUST_ANCHOR_BASE64_TEST) + + buildConfigField(STRING, "IDP_SCOPE_DEVRU", "e-rezept-dev openid") } buildConfigField(STRING, "BASE_SERVICE_URI", baseServiceUri) buildConfigField(STRING, "IDP_SERVICE_URI", idpServiceUri) @@ -303,6 +295,16 @@ buildkonfig { buildConfigField(BOOLEAN, "TEST_RUN_WITH_TRUSTSTORE_INTEGRATION", "false") buildConfigField(BOOLEAN, "TEST_RUN_WITH_IDP_INTEGRATION", "false") + + buildConfigField( + STRING, + "IDP_DEFAULT_SCOPE", + if (flavor.contains(Environments.DEVRU.name, true)) { + "e-rezept-dev openid" + } else { + "e-rezept openid" + } + ) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt index 28c9ee27..2c925a6b 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ErpService.kt @@ -22,6 +22,7 @@ import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.serialization.json.JsonElement import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST @@ -83,7 +84,24 @@ interface ErpService { @GET("MedicationDispense") suspend fun bundleOfMedicationDispenses( - @Tag profileName: String, + @Tag profileId: ProfileIdentifier, @Query("identifier") id: String ): Response + + @GET("Consent") + suspend fun getConsent( + @Tag profileId: ProfileIdentifier + ): Response + + @POST("Consent") + suspend fun grantConsent( + @Tag profileId: ProfileIdentifier, + @Body consent: JsonElement + ): Response + + @DELETE("Consent") + suspend fun deleteConsent( + @Tag profileId: ProfileIdentifier, + @Query("category") category: String + ): Response } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt new file mode 100644 index 00000000..f8001514 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.model + +import de.gematik.ti.erp.app.fhir.model.json +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedString +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.fhir.parser.profileValue +import kotlinx.datetime.Clock +import kotlinx.serialization.json.JsonElement + +private fun template( + insuranceIdentifier: String, + timeStamp: String +) = """ +{ + "resourceType": "Consent", + "meta": { + "profile": [ + "https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent|1.0" + ] + }, + "status": "active", + "scope": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/consentscope", + "code": "patient-privacy" + } + ] + }, + "category": [ + { + "coding": [ + { + "system": "https://gematik.de/fhir/erpchrg/CodeSystem/GEM_ERPCHRG_CS_ConsentType", + "code": "CHARGCONS" + } + ] + } + ], + "patient": { + "identifier": { + "system": "http://fhir.de/NamingSystem/gkv/kvid-10", + "value": "$insuranceIdentifier" + } + }, + "dateTime": "$timeStamp", + "policyRule": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "OPTIN" + } + ] + } +} +""".trimIndent() + +fun createConsent( + insuranceId: String +): JsonElement { + val templateString = template( + insuranceIdentifier = insuranceId, + timeStamp = Clock.System.now().asFhirTemporal().formattedString() + ) + + return json.parseToJsonElement(templateString) +} + +enum class ConsentType { + Charge +} +fun extractConsentBundle( + bundle: JsonElement, + save: ( + consent: List + ) -> Unit +) { + val resources = bundle + .findAll("entry.resource") + + val consents = resources.mapNotNull { resource -> + val profileString = resource + .contained("meta") + .contained("profile") + .contained() + + when { + profileValue( + "https://gematik.de/fhir/erpchrg/StructureDefinition/GEM_ERPCHRG_PR_Consent", + "1.0" + ).invoke( + profileString + ) -> extractConsent(bundle) + else -> error("unsupported profile") + } + }.toList() + save(consents) +} + +fun extractConsent(bundle: JsonElement) = + bundle.findAll("entry.resource.category.coding.code").firstOrNull()?.let { + when (it.containedString()) { + "CHARGCONS" -> ConsentType.Charge + else -> null + } + } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt new file mode 100644 index 00000000..9f578b8f --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRemoteDataSource.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.repository + +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.api.ErpService +import de.gematik.ti.erp.app.api.safeApiCall +import de.gematik.ti.erp.app.api.safeApiCallRaw +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.serialization.json.JsonElement +import java.net.HttpURLConnection + +class ConsentRemoteDataSource( + private val service: ErpService +) { + suspend fun getConsent( + profileId: ProfileIdentifier + ) = safeApiCall( + errorMessage = "Error getting consent information" + ) { + service.getConsent( + profileId = profileId + ) + } + + suspend fun grantConsent( + profileId: ProfileIdentifier, + consent: JsonElement + ) = safeApiCall( + errorMessage = "Error grant consent" + ) { + service.grantConsent( + profileId = profileId, + consent = consent + ) + } + + suspend fun deleteChargeConsent( + profileId: ProfileIdentifier + ) = safeApiCallRaw( + errorMessage = "Error delete consent" + ) { + val response = service.deleteConsent( + profileId = profileId, + category = "CHARGCONS" + ) + if (response.code() == HttpURLConnection.HTTP_NO_CONTENT) { + Result.success(Unit) + } else { + Result.failure( + ApiCallException( + "Expected no content but received: ${response.code()} ${response.message()}", + response + ) + ) + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt new file mode 100644 index 00000000..2ae901e9 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.repository + +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonElement + +class ConsentRepository( + private val remoteDataSource: ConsentRemoteDataSource, + private val dispatchers: DispatchProvider +) { + suspend fun getConsent( + profileId: ProfileIdentifier + ): Result = withContext(dispatchers.IO) { + remoteDataSource.getConsent( + profileId = profileId + ) + } + + suspend fun grantConsent( + profileId: ProfileIdentifier, + consent: JsonElement + ): Result = withContext(dispatchers.IO) { + remoteDataSource.grantConsent( + profileId = profileId, + consent = consent + ) + } + suspend fun deleteChargeConsent( + profileId: ProfileIdentifier + ): Result = withContext(dispatchers.IO) { + remoteDataSource.deleteChargeConsent( + profileId = profileId + ) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ConsentUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ConsentUseCase.kt new file mode 100644 index 00000000..48d38c1d --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/usecase/ConsentUseCase.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.consent.usecase + +import de.gematik.ti.erp.app.consent.model.ConsentType +import de.gematik.ti.erp.app.consent.model.createConsent +import de.gematik.ti.erp.app.consent.model.extractConsentBundle +import de.gematik.ti.erp.app.consent.repository.ConsentRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier + +class ConsentUseCase(private val consentRepository: ConsentRepository) { + + suspend fun getChargeConsent( + profileId: ProfileIdentifier + ) = consentRepository.getConsent( + profileId = profileId + ).map { + var granted = false + extractConsentBundle(it) { consentTypes -> + granted = consentTypes.any { consentType -> + consentType == ConsentType.Charge + } + } + granted + } + + suspend fun grantChargeConsent( + profileId: ProfileIdentifier, + insuranceId: String + ): Result { + val consent = createConsent(insuranceId) + return consentRepository.grantConsent( + profileId = profileId, + consent = consent + ) + } + + suspend fun deleteChargeConsent( + profileId: ProfileIdentifier + ): Result = + consentRepository.deleteChargeConsent( + profileId = profileId + ) +} 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 a1fcb522..e9979fbe 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 @@ -33,16 +33,7 @@ enum class SettingsAuthenticationMethodV1 { HealthCard, DeviceSecurity, Password, - Unspecified, - - @Deprecated("Keep for older app versions migrating to a newer one with mandatory app protection.") - Biometrics, - - @Deprecated("Keep for older app versions migrating to a newer one with mandatory app protection.") - DeviceCredentials, - - @Deprecated("Keep for older app versions migrating to a newer one with mandatory app protection.") - None + Unspecified } class PasswordEntityV1 : RealmObject { diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt index 034adbc9..a6eb935c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapper.kt @@ -57,10 +57,17 @@ fun extractOrganization( .firstOrNull() ?.containedString("value") + val iknr = resource + .findAll("identifier") + .filterWith("system", stringValue("http://fhir.de/sid/arge-ik/iknr")) + .firstOrNull() + ?.containedString("value") + return processOrganization( name, resource.extractAddress(processAddress), bsnr, + iknr, phone, mail ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt index c4860b64..94217704 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/KBVMapper.kt @@ -32,7 +32,8 @@ typealias AddressFn = ( typealias OrganizationFn = ( name: String?, address: Address, - uniqueIdentifier: String?, + bsnr: String?, + iknr: String?, phone: String?, mail: String? ) -> R @@ -160,7 +161,7 @@ fun extractkbvbundleversion102( + ) -> extractKBVBundleVersion102( bundle, processOrganization, processPatient, @@ -180,7 +181,7 @@ fun - extractkbvbundleversion110( + extractKBVBundleVersion110( bundle, processOrganization, processPatient, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PKVInvoiceMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PKVInvoiceMapper.kt new file mode 100644 index 00000000..a3519198 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PKVInvoiceMapper.kt @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.fhir.parser.FhirTemporal +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedDouble +import de.gematik.ti.erp.app.fhir.parser.containedString +import de.gematik.ti.erp.app.fhir.parser.filterWith +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.fhir.parser.isProfileValue +import de.gematik.ti.erp.app.fhir.parser.or +import de.gematik.ti.erp.app.fhir.parser.stringValue +import de.gematik.ti.erp.app.fhir.parser.toFhirTemporal +import kotlinx.serialization.json.JsonElement + +enum class SpecialPZN(val pzn: String) { + EmergencyServiceFee("02567018"), + BTMFee("02567001"), + TPrescriptionFee("06460688"), + ProvisioningCosts("09999637"), + DeliveryServiceCosts("06461110"); + + companion object { + fun isAnyOf(pzn: String): Boolean = values().any { it.pzn == pzn } + + fun valueOfPZN(pzn: String) = SpecialPZN.values().find { it.pzn == pzn } + } +} + +data class ChargeableItem(val description: Description, val factor: Double, val price: PriceComponent) { + sealed interface Description { + data class PZN(val pzn: String) : Description { + fun isSpecialPZN() = SpecialPZN.isAnyOf(pzn) + } + + data class TA1(val ta1: String) : Description + + data class HMNR(val hmnr: String) : Description + } +} + +data class PriceComponent(val value: Double, val tax: Double) + +typealias PkvDispenseFn = ( + whenHandedOver: FhirTemporal +) -> R + +typealias InvoiceFn = ( + totalAdditionalFee: Double, + totalBruttoAmount: Double, + currency: String, + items: List +) -> R + +fun extractPKVInvoiceBundle( + bundle: JsonElement, + processDispense: PkvDispenseFn, + processPharmacyAddress: AddressFn, + processPharmacy: OrganizationFn, + processInvoice: InvoiceFn, + save: ( + pharmacy: Pharmacy, + invoice: Invoice, + dispense: Dispense + ) -> R +): R? { + val profileString = bundle.contained("meta").contained("profile").contained() + return when { + profileString.isProfileValue( + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-AbgabedatenBundle", + "1.1" + ) -> extractPKVInvoiceBundleVersion11( + bundle, + processDispense, + processPharmacyAddress, + processPharmacy, + processInvoice, + save + ) + + else -> null + } +} + +fun extractPKVInvoiceBundleVersion11( + bundle: JsonElement, + processDispense: PkvDispenseFn, + processPharmacyAddress: AddressFn, + processPharmacy: OrganizationFn, + processInvoice: InvoiceFn, + save: ( + pharmacy: Pharmacy, + invoice: Invoice, + dispense: Dispense + ) -> R +): R { + val resources = bundle + .findAll("entry.resource") + + var dispense: Dispense? = null + var pharmacy: Pharmacy? = null + var invoice: Invoice? = null + + resources.forEach { resource -> + val profileString = resource + .contained("meta") + .contained("profile") + .contained() + + when { + profileString.isProfileValue( + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Abgabeinformationen", + "1.1" + ) -> { + dispense = extractPkvDispense( + resource, + processDispense + ) + } + + profileString.isProfileValue( + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Apotheke", + "1.1" + ) -> { + pharmacy = extractOrganization( + resource, + processPharmacy, + processPharmacyAddress + ) + } + + profileString.isProfileValue( + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Abrechnungszeilen", + "1.1" + ) -> { + invoice = extractInvoice( + resource, + processInvoice + ) + } + } + } + + return save( + requireNotNull(pharmacy) { "Pharmacy missing" }, + requireNotNull(invoice) { "Invoice missing" }, + requireNotNull(dispense) { "Dispense missing" } + ) +} + +fun extractPkvDispense( + dispense: JsonElement, + processDispense: PkvDispenseFn +): Dispense { + return processDispense( + dispense.containedString("whenHandedOver").toFhirTemporal() + ) +} + +fun extractInvoice( + invoice: JsonElement, + processInvoice: InvoiceFn +): Invoice { + val totalGross = invoice.contained("totalGross") + val currency = totalGross.containedString("currency") + val totalBruttoAmount = totalGross.containedDouble("value") + + val totalAdditionalFee = invoice + .findAll("totalGross.extension") + .filterWith( + "url", + stringValue("http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-Gesamtzuzahlung") + ) + .first() + .contained("valueMoney") + .containedDouble("value") + + val items = invoice + .findAll("lineItem") + .mapNotNull { lineItem -> + val value = lineItem + .contained("priceComponent") + .contained("amount") + .containedDouble("value") + + val tax = lineItem + .findAll("priceComponent.extension") + .filterWith( + "url", + stringValue("http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-MwStSatz") + ) + .first() + .containedDouble("valueDecimal") + + val factor = lineItem + .contained("priceComponent") + .containedDouble("factor") + + val item = lineItem + .findAll("chargeItemCodeableConcept.coding") + .filterWith( + "system", + or( + stringValue("http://fhir.de/CodeSystem/ifa/pzn"), + stringValue("http://TA1.abda.de"), + stringValue("http://fhir.de/sid/gkv/hmnr") + ) + ) + .firstOrNull() + ?.let { + val code = it.containedString("code") + val price = PriceComponent(value, tax) + when (it.containedString("system")) { + "http://fhir.de/CodeSystem/ifa/pzn" -> + ChargeableItem(ChargeableItem.Description.PZN(code), factor, price) + + "http://TA1.abda.de" -> + ChargeableItem(ChargeableItem.Description.TA1(code), factor, price) + + "http://fhir.de/sid/gkv/hmnr" -> + ChargeableItem(ChargeableItem.Description.HMNR(code), factor, price) + + else -> null + } + } + + item + }.toList() + + return processInvoice( + totalAdditionalFee, + totalBruttoAmount, + currency, + items + ) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt index 124a0a1b..280a158a 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_0_2.kt @@ -35,7 +35,7 @@ import kotlinx.serialization.json.jsonPrimitive @Suppress("LongParameterList", "LongMethod") fun extractkbvbundleversion102( + Medication, Ingredient, MultiplePrescriptionInfo, Quantity, Ratio, Address> extractKBVBundleVersion102( bundle: JsonElement, processOrganization: OrganizationFn, processPatient: PatientFn, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt index c02c9c89..4e6b66d5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/ResourceMapperVersion_1_1_0.kt @@ -35,7 +35,7 @@ import kotlinx.serialization.json.jsonPrimitive @Suppress("LongParameterList", "LongMethod") fun extractkbvbundleversion110( + Medication, Ingredient, MultiplePrescriptionInfo, Quantity, Ratio, Address> extractKBVBundleVersion110( bundle: JsonElement, processOrganization: OrganizationFn, processPatient: PatientFn, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt index ac934d00..71a57fee 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt @@ -25,7 +25,6 @@ import de.gematik.ti.erp.app.idp.EcdsaUsingShaAlgorithmExtending.EcdsaBP384R1Usi import de.gematik.ti.erp.app.idp.EcdsaUsingShaAlgorithmExtending.EcdsaBP512R1UsingSha512 import org.jose4j.jwa.AlgorithmFactoryFactory import org.jose4j.keys.EllipticCurves -import java.lang.reflect.InvocationTargetException import java.math.BigInteger import java.security.spec.ECFieldFp import java.security.spec.ECParameterSpec @@ -77,41 +76,24 @@ object EllipticCurvesExtending : EllipticCurves() { ) private var initializedInSession = false - @Throws( - InvocationTargetException::class, - IllegalAccessException::class, - NoSuchMethodException::class - ) - private fun addCurve(name: String, spec: ECParameterSpec) { - val method = EllipticCurves::class.java.getDeclaredMethod( - "addCurve", - String::class.java, - ECParameterSpec::class.java + fun init(): Boolean = if (initializedInSession) { + true + } else try { + addCurve("BP-256", BP256) + addCurve("BP-384", BP384) + addCurve("BP-512", BP512) + AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( + EcdsaBP256R1UsingSha256() ) - method.isAccessible = true - method.invoke(EllipticCurvesExtending::class.java, name, spec) - } - - fun init(): Boolean { - return if (initializedInSession) { - true - } else try { - addCurve("BP-256", BP256) - addCurve("BP-384", BP384) - addCurve("BP-512", BP512) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP256R1UsingSha256() - ) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP384R1UsingSha384() - ) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP512R1UsingSha512() - ) - initializedInSession = true - true - } catch (e: Exception) { - throw IllegalStateException("failure on init $e") - } + AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( + EcdsaBP384R1UsingSha384() + ) + AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( + EcdsaBP512R1UsingSha512() + ) + initializedInSession = true + true + } catch (e: Exception) { + throw IllegalStateException("failure on init $e") } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt index 7cadfa52..2bd6485d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRemoteDataSource.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.idp.repository -import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.api.ApiCallException import de.gematik.ti.erp.app.api.safeApiCall import de.gematik.ti.erp.app.api.safeApiCallRaw @@ -28,11 +27,11 @@ import okhttp3.ResponseBody import retrofit2.Response import java.net.HttpURLConnection -private val defaultScope = BuildKonfig.IDP_DEFAULT_SCOPE private const val pairingScope = "pairing openid" -class IdpRemoteDataSource constructor( - private val service: IdpService +class IdpRemoteDataSource( + private val service: IdpService, + private val defaultScope: () -> String ) { suspend fun fetchDiscoveryDocument() = @@ -65,7 +64,7 @@ class IdpRemoteDataSource constructor( codeChallenge = codeChallenge, nonce = nonce, state = state, - scope = if (isPairingScope) pairingScope else defaultScope + scope = if (isPairingScope) pairingScope else defaultScope() ) } @@ -83,7 +82,7 @@ class IdpRemoteDataSource constructor( codeChallenge = codeChallenge, state = state, nonce = nonce, - scope = if (isDeviceRegistration) pairingScope else defaultScope, + scope = if (isDeviceRegistration) pairingScope else defaultScope(), redirectUri = redirectUri ) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/HtmlTemplate.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/HtmlTemplate.kt new file mode 100644 index 00000000..a39cbd62 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/HtmlTemplate.kt @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.invoice.usecase + +import de.gematik.ti.erp.app.fhir.model.ChargeableItem +import de.gematik.ti.erp.app.fhir.model.SpecialPZN + +object PkvHtmlTemplate { + fun createOrganization( + organizationName: String, + organizationAddress: String, + organizationIKNR: String? + ) = "$organizationName
$organizationAddress${organizationIKNR?.let { "
IKNR: $it" } ?: ""}" + + fun createPrescriber( + prescriberName: String, + prescriberAddress: String, + prescriberLANR: String + ) = "$prescriberName
$prescriberAddress
LANR: $prescriberLANR" + + fun createPatient( + patientName: String, + patientAddress: String, + patientKVNR: String + ) = "$patientName
$patientAddress
KVNR: $patientKVNR" + + fun createArticle( + article: String, + factor: Double, + tax: Double, + bruttoAmount: Double + ) = """ +
$article
+
${factor.currencyString()}
+
${tax.currencyString()}%
+
${bruttoAmount.currencyString()}
+ """.trimIndent() + + fun createPriceData( + currency: String, + totalBruttoAmount: Double, + items: List + ): String { + val (fees, articles) = items.partition { + (it.description as? ChargeableItem.Description.PZN)?.isSpecialPZN() ?: false + } + + return createPriceData( + currency = currency, + totalBruttoAmount = totalBruttoAmount, + articles = articles.map { + val article = when (it.description) { + is ChargeableItem.Description.HMNR -> "HMKNR ${it.description.hmnr}" + is ChargeableItem.Description.PZN -> "PZN ${it.description.pzn}" + is ChargeableItem.Description.TA1 -> "TA1 ${it.description.ta1}" + } + + createArticle( + article = article, + factor = it.factor, + tax = it.price.tax, + bruttoAmount = it.price.value + ) + }, + fees = fees.map { + require(it.description is ChargeableItem.Description.PZN) + val article = when (SpecialPZN.valueOfPZN(it.description.pzn)) { + SpecialPZN.EmergencyServiceFee -> "Notdienstgebühr" + SpecialPZN.BTMFee -> "BTM-Gebühr" + SpecialPZN.TPrescriptionFee -> "T-Rezept Gebühr" + SpecialPZN.ProvisioningCosts -> "Beschaffungskosten" + SpecialPZN.DeliveryServiceCosts -> "Botendienst" + null -> error("wrong mapping") + } + createArticle( + article = article, + factor = it.factor, + tax = it.price.tax, + bruttoAmount = it.price.value + ) + } + ) + } + + fun createPriceData( + currency: String, + totalBruttoAmount: Double, + articles: List, + fees: List + ) = """ +
+
Kosten
+
+
Artikel
+
Anzahl
+
MwSt.
+
Bruttopreis in $currency
+ ${articles.joinToString("")} +
Zusätzliche Gebühren
+
+
+
+ ${fees.joinToString("")} +
Gesamtsumme
+
+
+
${totalBruttoAmount.currencyString()}
+
+
+ """.trimIndent() +} + +@Suppress("LongParameterList", "LongMethod") +fun createPkvHtmlInvoiceTemplate( + patient: String, + patientBirthdate: String, + prescriber: String, + prescribedOn: String, + organization: String, + dispensedOn: String, + priceData: String +) = """ + + + + + Abrechnung zur Vorlage bei Kostenträger + + + +

PDF für Privatversicherte zur Abrechnung Ihres E-Rezeptes

+ Bitte reichen Sie diesen Beleg als PDF bei Ihrem Kostenträger ein +
+
Patient
+
+
+ $patient +
+
+ Geb. $patientBirthdate +
+
+
+
+
+
Aussteller
+
+
+ $prescriber +
+
+ ausgestellt am: +
+
+ $prescribedOn +
+
+
+
+
Eingelöst
+
+
+ $organization +
+
+ abgegeben am: +
+
+ $dispensedOn +
+
+
+
+ $priceData + + +""".trimIndent() + +private fun Double.currencyString() = "%.2f".format(this) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt index 3d6974c5..edfb8651 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt @@ -140,11 +140,11 @@ class TaskLocalDataSource( try { extractKBVBundle( bundleResource, - processOrganization = { name, address, uniqueIdentifier, phone, mail -> + processOrganization = { name, address, bsnr, iknr, phone, mail -> OrganizationEntityV1().apply { this.name = name this.address = address - this.uniqueIdentifier = uniqueIdentifier + this.uniqueIdentifier = bsnr this.phone = phone this.mail = mail } 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 4ce1c0e1..0cea19ec 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 @@ -78,14 +78,5 @@ object SettingsData { } object Unspecified : AuthenticationMode() - - @Deprecated("replaced by deviceSecurity") - object Biometrics : AuthenticationMode() - - @Deprecated("replaced by deviceSecurity") - object DeviceCredentials : AuthenticationMode() - - @Deprecated("not available anymore") - object None : 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 244a082b..5d5dfe12 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 @@ -83,11 +83,6 @@ class SettingsRepository constructor( ) } } - - SettingsAuthenticationMethodV1.Biometrics -> SettingsData.AuthenticationMode.Biometrics - SettingsAuthenticationMethodV1.DeviceCredentials -> - SettingsData.AuthenticationMode.DeviceCredentials - SettingsAuthenticationMethodV1.None -> SettingsData.AuthenticationMode.None else -> SettingsData.AuthenticationMode.Unspecified } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt index 3de9544d..ef0b2b29 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommonRessourceMapperTest.kt @@ -38,10 +38,11 @@ class CommonRessourceMapperTest { ReturnType.Address }, - processOrganization = { name, address, uniqueIdentifier, phone, mail -> + processOrganization = { name, address, bsnr, iknr, phone, mail -> assertEquals("MVZ", name) assertEquals(ReturnType.Address, address) - assertEquals("721111100", uniqueIdentifier) + assertEquals("721111100", bsnr) + assertEquals(null, iknr) assertEquals("0301234567", phone) assertEquals("mvz@e-mail.de", mail) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PKVMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PKVMapperTest.kt new file mode 100644 index 00000000..f0c77fdf --- /dev/null +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/PKVMapperTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.fhir.model + +import de.gematik.ti.erp.app.fhir.parser.asFhirTemporal +import kotlinx.datetime.LocalDate +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals + +enum class PKVReturnType { + InvoiceBundle, Invoice, Pharmacy, PharmacyAddress, Dispense +} + +class PKVMapperTest { + @Test + fun `process pkv bundle version 1_1`() { + val bundle = Json.parseToJsonElement(pkvAbgabedatenJson_vers_1_1) + val result = extractPKVInvoiceBundle( + bundle, + processInvoice = { totalAdditionalFee, totalBruttoAmount, currency, items -> + assertEquals(0.0, totalAdditionalFee) + assertEquals(51.48, totalBruttoAmount) + assertEquals("EUR", currency) + + assertEquals( + ChargeableItem(ChargeableItem.Description.PZN("11514676"), 2.0, PriceComponent(48.98, 19.0)), + items[0] + ) + assertEquals(false, (items[0].description as ChargeableItem.Description.PZN).isSpecialPZN()) + + assertEquals( + ChargeableItem(ChargeableItem.Description.PZN("02567018"), 1.0, PriceComponent(2.50, 19.0)), + items[1] + ) + assertEquals(true, (items[1].description as ChargeableItem.Description.PZN).isSpecialPZN()) + + PKVReturnType.Invoice + }, + processDispense = { whenHandedOver -> + assertEquals(LocalDate.parse("2022-03-25").asFhirTemporal(), whenHandedOver) + + PKVReturnType.Dispense + }, + processPharmacyAddress = { line, postalCode, city -> + assertEquals(listOf("Taunusstraße 89"), line) + assertEquals("63225", postalCode) + assertEquals("Langen", city) + + PKVReturnType.PharmacyAddress + }, + processPharmacy = { name, address, bsnr, iknr, phone, mail -> + assertEquals("Adler-Apotheke", name) + assertEquals(PKVReturnType.PharmacyAddress, address) + assertEquals(null, bsnr) + assertEquals("123456789", iknr) + assertEquals(null, phone) + assertEquals(null, mail) + + PKVReturnType.Pharmacy + }, + save = { pharmacy, invoice, dispense -> + assertEquals(PKVReturnType.Pharmacy, pharmacy) + assertEquals(PKVReturnType.Invoice, invoice) + assertEquals(PKVReturnType.Dispense, dispense) + + PKVReturnType.InvoiceBundle + } + ) + + assertEquals(PKVReturnType.InvoiceBundle, result) + } +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt index ba138501..249ea57b 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/RessourceMapperVersion102Test.kt @@ -26,6 +26,7 @@ import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue + enum class ReturnType { Organization, Patient, Practitioner, InsuranceInformation, MedicationRequest, MedicationDispense, Medication, Ingredient, MultiplePrescriptionInfo, Quantity, Ratio, Address diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt similarity index 97% rename from common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt rename to common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt index 43ada2db..7931ff8a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestDataKBVMapper.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TestData.kt @@ -127,3 +127,7 @@ val medDispenseBundleVersion_1_2 by lazy { val task_bundle_version_1_2 by lazy { File("$ResourceBasePath/fhir/task_bundle_vers_1_2.json").readText() } + +val pkvAbgabedatenJson_vers_1_1 by lazy { + File("$ResourceBasePath/fhir/pkv_abgabedaten_1_1.json").readText() +} diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt index 321eb566..06bf0ab1 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpIntegrationTest.kt @@ -127,7 +127,7 @@ class IdpIntegrationTest { idpRepository = spyk( IdpRepository( - remoteDataSource = IdpRemoteDataSource(idpService), + remoteDataSource = IdpRemoteDataSource(idpService) { BuildKonfig.IDP_DEFAULT_SCOPE }, localDataSource = localDataSource ) ) diff --git a/common/src/commonTest/resources/fhir/pkv_abgabedaten_1_1.json b/common/src/commonTest/resources/fhir/pkv_abgabedaten_1_1.json new file mode 100644 index 00000000..033a9cd0 --- /dev/null +++ b/common/src/commonTest/resources/fhir/pkv_abgabedaten_1_1.json @@ -0,0 +1,378 @@ +{ + "resourceType": "Bundle", + "id": "f548dde3-a319-486b-8624-6176ff41ad90", + "meta": { + "profile": [ + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-AbgabedatenBundle|1.1" + ], + "tag": [ + { + "display": "Beispiel RezeptAbgabedatenPKV Bundle (FAM + Noctu + Rezeptänderung)" + }, + { + "display": "ACHTUNG! Der fachlich korrekte Inhalt der Beispielinstanz kann nicht gewährleistet werden. Wir sind jederzeit dankbar für Hinweise auf Fehler oder für Verbesserungsvorschläge." + } + ] + }, + "type": "document", + "identifier": { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "200.100.000.000.082.87" + }, + "timestamp": "2022-03-25T23:40:00Z", + "entry": [ + { + "resource": { + "resourceType": "Composition", + "id": "4dc5f425-b9b6-4e39-9166-42668ead6c86", + "meta": { + "profile": [ + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-AbgabedatenComposition|1.1" + ] + }, + "status": "final", + "type": { + "coding": [ + { + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-CS-ERP-CompositionTypes", + "code": "ERezeptAbgabedaten" + } + ] + }, + "title": "ERezeptAbgabedaten", + "section": [ + { + "title": "Abgabeinformationen", + "entry": [ + { + "reference": "urn:uuid:37a647b8-cb89-491a-af0f-f9bffc2b386c" + } + ] + }, + { + "title": "Apotheke", + "entry": [ + { + "reference": "urn:uuid:1fa57d53-812b-4cab-a42e-94a12481108a" + } + ] + } + ], + "date": "2022-03-25T23:40:00Z", + "author": [ + { + "reference": "urn:uuid:1fa57d53-812b-4cab-a42e-94a12481108a" + } + ] + }, + "fullUrl": "urn:uuid:4dc5f425-b9b6-4e39-9166-42668ead6c86" + }, + { + "resource": { + "resourceType": "Organization", + "id": "1fa57d53-812b-4cab-a42e-94a12481108a", + "meta": { + "profile": [ + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Apotheke|1.1" + ] + }, + "identifier": [ + { + "system": "http://fhir.de/sid/arge-ik/iknr", + "value": "123456789" + } + ], + "address": [ + { + "type": "physical", + "line": [ + "Taunusstraße 89" + ], + "_line": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-streetName", + "valueString": "Taunusstraße" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-houseNumber", + "valueString": "89" + } + ] + } + ], + "city": "Langen", + "postalCode": "63225", + "country": "D" + } + ], + "name": "Adler-Apotheke" + }, + "fullUrl": "urn:uuid:1fa57d53-812b-4cab-a42e-94a12481108a" + }, + { + "resource": { + "resourceType": "MedicationDispense", + "id": "37a647b8-cb89-491a-af0f-f9bffc2b386c", + "meta": { + "profile": [ + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Abgabeinformationen|1.1" + ] + }, + "status": "completed", + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/data-absent-reason", + "code": "not-applicable" + } + ] + }, + "type": { + "coding": [ + { + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-CS-ERP-MedicationDispenseTyp", + "code": "Abgabeinformationen" + } + ] + }, + "extension": [ + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-Abrechnungszeilen", + "valueReference": { + "reference": "urn:uuid:8a99bfa5-f7aa-4741-99d8-f1abbd301ae1" + } + }, + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-EX-ERP-AbrechnungsTyp", + "valueCodeableConcept": { + "coding": [ + { + "code": "1", + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-PKV-CS-ERP-AbrechnungsTyp" + } + ] + } + } + ], + "authorizingPrescription": [ + { + "identifier": { + "system": "https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId", + "value": "200.100.000.000.082.87" + } + } + ], + "substitution": { + "extension": [ + { + "extension": [ + { + "url": "ArtRezeptaenderung", + "valueCodeableConcept": { + "coding": [ + { + "code": "21", + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-PKV-CS-ERP-ArtRezeptaenderung" + } + ] + } + }, + { + "url": "RueckspracheArzt", + "valueCodeableConcept": { + "coding": [ + { + "code": "2", + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-CS-ERP-RueckspracheArzt" + } + ] + } + } + ], + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-Rezeptaenderung" + } + ], + "wasSubstituted": true + }, + "performer": [ + { + "actor": { + "reference": "urn:uuid:1fa57d53-812b-4cab-a42e-94a12481108a" + } + } + ], + "whenHandedOver": "2022-03-25" + }, + "fullUrl": "urn:uuid:37a647b8-cb89-491a-af0f-f9bffc2b386c" + }, + { + "resource": { + "resourceType": "Invoice", + "id": "8a99bfa5-f7aa-4741-99d8-f1abbd301ae1", + "meta": { + "profile": [ + "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-PKV-PR-ERP-Abrechnungszeilen|1.1" + ] + }, + "status": "issued", + "type": { + "coding": [ + { + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-CS-ERP-InvoiceTyp", + "code": "Abrechnungszeilen" + } + ] + }, + "lineItem": [ + { + "priceComponent": [ + { + "type": "informational", + "extension": [ + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-MwStSatz", + "valueDecimal": 19 + }, + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-KostenVersicherter", + "extension": [ + { + "url": "Kategorie", + "valueCodeableConcept": { + "coding": [ + { + "code": "0", + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-PKV-CS-ERP-KostenVersicherterKategorie" + } + ] + } + }, + { + "url": "Kostenbetrag", + "valueMoney": { + "currency": "EUR", + "value": 0 + } + } + ] + } + ], + "amount": { + "currency": "EUR", + "value": 48.98 + }, + "factor": 2 + } + ], + "sequence": 1, + "chargeItemCodeableConcept": { + "coding": [ + { + "code": "11514676", + "system": "http://fhir.de/CodeSystem/ifa/pzn" + } + ] + } + }, + { + "priceComponent": [ + { + "type": "informational", + "extension": [ + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-MwStSatz", + "valueDecimal": 19 + }, + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-KostenVersicherter", + "extension": [ + { + "url": "Kategorie", + "valueCodeableConcept": { + "coding": [ + { + "code": "0", + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-PKV-CS-ERP-KostenVersicherterKategorie" + } + ] + } + }, + { + "url": "Kostenbetrag", + "valueMoney": { + "currency": "EUR", + "value": 0 + } + } + ] + } + ], + "amount": { + "currency": "EUR", + "value": 2.5 + }, + "factor": 1 + } + ], + "extension": [ + { + "extension": [ + { + "extension": [ + { + "url": "Gruppe", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://fhir.abda.de/eRezeptAbgabedaten/CodeSystem/DAV-CS-ERP-ZusatzattributGruppe", + "code": "11" + } + ] + } + }, + { + "url": "DatumUhrzeit", + "valueDateTime": "2022-03-25T23:30:00.0Z" + }, + { + "url": "Schluessel", + "valueBoolean": true + } + ], + "url": "ZusatzattributAbgabeNoctu" + } + ], + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-Zusatzattribute" + } + ], + "sequence": 2, + "chargeItemCodeableConcept": { + "coding": [ + { + "code": "02567018", + "system": "http://fhir.de/CodeSystem/ifa/pzn" + } + ] + } + } + ], + "totalGross": { + "currency": "EUR", + "extension": [ + { + "url": "http://fhir.abda.de/eRezeptAbgabedaten/StructureDefinition/DAV-EX-ERP-Gesamtzuzahlung", + "valueMoney": { + "currency": "EUR", + "value": 0 + } + } + ], + "value": 51.48 + } + }, + "fullUrl": "urn:uuid:8a99bfa5-f7aa-4741-99d8-f1abbd301ae1" + } + ] +} \ No newline at end of file diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Constants.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/Constants.kt similarity index 100% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Constants.kt rename to common/src/desktopMain/kotlin/de/gematik/ti/erp/app/Constants.kt diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt similarity index 100% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt rename to common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/vau/di/VauModule.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt similarity index 95% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/vau/di/VauModule.kt rename to common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt index 6c35e1d3..a076b898 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/vau/di/VauModule.kt +++ b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/di/VauModule.kt @@ -16,10 +16,9 @@ * */ -package de.gematik.ti.erp.app.vau.di +package de.gematik.ti.erp.app.di import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.di.ScopedRealm import de.gematik.ti.erp.app.vau.api.model.UntrustedCertList import de.gematik.ti.erp.app.vau.api.model.UntrustedOCSPList import de.gematik.ti.erp.app.vau.interceptor.DefaultCryptoConfig @@ -31,6 +30,8 @@ import de.gematik.ti.erp.app.vau.usecase.TrustedTruststore import de.gematik.ti.erp.app.vau.usecase.TruststoreConfig import de.gematik.ti.erp.app.vau.usecase.TruststoreTimeSourceProvider import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.bouncycastle.cert.X509CertificateHolder import org.kodein.di.DI import org.kodein.di.bind @@ -39,8 +40,7 @@ import org.kodein.di.bindings.Scope import org.kodein.di.instance import org.kodein.di.scoped import org.kodein.di.singleton -import java.time.Duration -import java.time.Instant +import kotlin.time.Duration fun vauModule(scope: Scope) = DI.Module("VAU Module") { bindInstance { @@ -58,7 +58,7 @@ fun vauModule(scope: Scope) = DI.Module("VAU Module") { bind { scoped(scope).singleton { DefaultCryptoConfig() } } bind { scoped(scope).singleton { VauChannelInterceptor(instance(), instance(), instance()) } } bind { scoped(scope).singleton { TruststoreUseCase(instance(), instance(), instance(), instance()) } } - bind { scoped(scope).singleton { { Instant.now() } } } + bind { scoped(scope).singleton { { Clock.System.now() } } } bind { scoped(scope).singleton { { untrustedOCSPList: UntrustedOCSPList, diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt similarity index 100% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt rename to common/src/desktopMain/kotlin/de/gematik/ti/erp/app/utils/FileUtils.kt diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt similarity index 100% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt rename to common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index fe00941e..4d566984 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -10,7 +10,6 @@ ComplexCondition:LoginWithHealthCardScreen.kt$(can.length == it || it == 5 && can.length == 6) && isFocussed ComplexCondition:LoginWithHealthCardScreen.kt$triggerAuth && state.firstVisibleItemIndex == 4 && cardAccessNumber.isNotBlank() && personalIdentificationNumber.isNotBlank() ComplexCondition:Workarounds.kt$Workarounds$osName == "Mac OS X" && majorJavaVersion <= 16 && (majorOsVersion == 11 || majorOsVersion == 12) - ComplexMethod:SyncedTaskEntityV1Test.kt$SyncedTaskEntityV1Test$@Test fun `cascading delete`() EmptyDefaultConstructor:PrefixedLogger.kt$NapierLogger$() EmptyFunctionBlock:DebugScreenWrapper.kt${} EmptyFunctionBlock:Main.kt$LogHandler${ } @@ -42,18 +41,8 @@ MagicNumber:BasicData.kt$3 MagicNumber:BasicData.kt$4 MagicNumber:BasicData.kt$IdpNonce.Companion$32 - MagicNumber:CardWallAuthDialog.kt$0.3f - MagicNumber:CardWallAuthDialog.kt$0.7f - MagicNumber:CardWallAuthDialog.kt$1.1f - MagicNumber:CardWallAuthDialog.kt$10 MagicNumber:CardWallAuthDialog.kt$1000 - MagicNumber:CardWallAuthDialog.kt$1300 - MagicNumber:CardWallAuthDialog.kt$1500 - MagicNumber:CardWallAuthDialog.kt$2500 - MagicNumber:CardWallAuthDialog.kt$300 - MagicNumber:CardWallAuthDialog.kt$3000 MagicNumber:CardWallAuthDialog.kt$5000 - MagicNumber:CardWallAuthDialog.kt$600 MagicNumber:CardWallComponents.kt$6 MagicNumber:CardWallComponents.kt$8 MagicNumber:CardWallNfcInstructionScreen.kt$1.5f @@ -76,11 +65,6 @@ MagicNumber:EcdsaUsingShaAlgorithmExtending.kt$EcdsaUsingShaAlgorithmExtending.EcdsaBP256R1UsingSha256$64 MagicNumber:EcdsaUsingShaAlgorithmExtending.kt$EcdsaUsingShaAlgorithmExtending.EcdsaBP384R1UsingSha384$64 MagicNumber:EcdsaUsingShaAlgorithmExtending.kt$EcdsaUsingShaAlgorithmExtending.EcdsaBP512R1UsingSha512$64 - MagicNumber:EditShippingContactScreen.kt$4 - MagicNumber:EditShippingContactScreen.kt$5 - MagicNumber:EditShippingContactScreen.kt$6 - MagicNumber:EditShippingContactScreen.kt$7 - MagicNumber:EditShippingContactScreen.kt$8 MagicNumber:FileIdentifier.kt$FileIdentifier$0x011C MagicNumber:FileIdentifier.kt$FileIdentifier$0x1000 MagicNumber:FileIdentifier.kt$FileIdentifier$0x3FFF @@ -189,8 +173,8 @@ MagicNumber:Workarounds.kt$Workarounds$11 MagicNumber:Workarounds.kt$Workarounds$12 MagicNumber:Workarounds.kt$Workarounds$16 - MandatoryBracesIfStatements:EllipticCurvesExtending.kt$EllipticCurvesExtending$try { addCurve("BP-256", BP256) addCurve("BP-384", BP384) addCurve("BP-512", BP512) AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( EcdsaBP256R1UsingSha256() ) AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( EcdsaBP384R1UsingSha384() ) AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( EcdsaBP512R1UsingSha512() ) initializedInSession = true true } catch (e: Exception) { throw IllegalStateException("failure on init $e") } - MandatoryBracesIfStatements:LoginWithHealthCardScreen.kt$5 + MandatoryBracesIfStatements:EllipticCurvesExtending.kt$EllipticCurvesExtending$else + MandatoryBracesIfStatements:LoginWithHealthCardScreen.kt$if MaxLineLength:CardUtilitiesTest.kt$CardUtilitiesTest$"(4e2778f6aaef54cb42865a3c30c753495af4e53121400802d0ab1acd665e9c77,4c2fae1687e9daa36c64570c909f93176f01eeafcb45f9c08e49805f127d94ef,1,7d5a0975fc2c3057eef67530417affe7fb8055c126dc5c6ce94a4b44f330b5d9)" MaxLineLength:CardUtilitiesTest.kt$CardUtilitiesTest$Hex.decode("041B05278F276BD92E6B0EE3478BD3A93B03FE8E4C35556F0D6C13C89C504F91C065E85C1D289B306F61BE2CECCED4E7532BF0925A4907F246DF7A69C8D69ED24F") MaxLineLength:CardUtilitiesTest.kt$CardUtilitiesTest$Hex.decode("7C438341041B05278F276BD92E6B0EE3478BD3A93B03FE8E4C35556F0D6C13C89C504F91C065E85C1D289B306F61BE2CECCED4E7532BF0925A4907F246DF7A69C8D69ED24F") @@ -327,10 +311,6 @@ MaxLineLength:TestData.kt$TestCertificates.OCSP2$"MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAxWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBuyypCGo7gAAYDzIwMjEwNTE3MDYyMzAxWqARGA8yMDIxMDUxNzA2MjMwMVqhIzAhMB8GCSsGAQUFBzABAgQSBBDIsivTG9WljP4InmqVdKQmMAkGByqGSM49BAEDRwAwRAIgZMCyRhqMOaEG10KPz3mL5Yh7oX9fiIdBl8WrxLT2SewCIEvjzedVlnbt/j4e7VALo2xl8wvOcYe8gT04+PqH5vkfoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" MaxLineLength:TestData.kt$TestCertificates.OCSP3$"MIIEFgoBAKCCBA8wggQLBgkrBgEFBQcwAQEEggP8MIID+DCB+6FZMFcxCzAJBgNVBAYTAkRFMRowGAYDVQQKDBFnZW1hdGlrIE5PVC1WQUxJRDEsMCoGA1UEAwwjT0NTUCBTaWduZXIgS29tcC1DQTEwIGVjYyBURVNULU9OTFkYDzIwMjEwNTE3MDYyMzAwWjBoMGYwPjAHBgUrDgMCGgQUXI3/vEvbfD7eUzpI1js5mj5OUC4EFCjw+OapyHfMQ0Xbmq7XOoOsDg+oAgcBPCti7yC3gAAYDzIwMjEwNTE3MDYyMzAwWqARGA8yMDIxMDUxNzA2MjMwMFqhIzAhMB8GCSsGAQUFBzABAgQSBBAWpjYsPzj/U96/S1MvypTWMAkGByqGSM49BAEDRwAwRAIgXfEC3h/1H2/aHGEyJY9L59S6NbqdkStBBk2vczj+3mwCIASMGDqPuhA7ZLBJ5HhHpwKYEQw/YPluyBMnz7j2dXtPoIICojCCAp4wggKaMIICQKADAgECAgcDrTZEREJrMAoGCCqGSM49BAMCMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMB4XDTIwMDUwNjAwMDAwMFoXDTIzMDUwNjIzNTk1OVowVzELMAkGA1UEBhMCREUxGjAYBgNVBAoMEWdlbWF0aWsgTk9ULVZBTElEMSwwKgYDVQQDDCNPQ1NQIFNpZ25lciBLb21wLUNBMTAgZWNjIFRFU1QtT05MWTBaMBQGByqGSM49AgEGCSskAwMCCAEBBwNCAAQbB+RoQs3RyvkN+o/Vs2xZlLZeK4fqt4s9kscFIuTIWn9LNMq80N2bucqWno2iuDXPWeOHJqlGtpoLNFLpWt4Po4HHMIHEMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMA4GA1UdDwEB/wQEAwIGQDAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMBMGA1UdJQQMMAoGCCsGAQUFBwMJMAwGA1UdEwEB/wQCMAAwOAYIKwYBBQUHAQEELDAqMCgGCCsGAQUFBzABhhxodHRwOi8vZWhjYS5nZW1hdGlrLmRlL29jc3AvMB0GA1UdDgQWBBQcqomWuxucPpsAjIXZZJyse1SZ4jAKBggqhkjOPQQDAgNIADBFAiEAiR3T/gS0MtFGdvQHyaCa+XF6lisspf+WRUahfLQPrg4CICQ4KZc7AYkTFnHFGbh2In8Y/Nkjh5wCdpWqeKRgBBUJ" MaxLineLength:TestData.kt$TestCertificates.Vau$"MIIC7jCCApWgAwIBAgIHATwrYu8gtzAKBggqhkjOPQQDAjCBhDELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxMjAwBgNVBAsMKUtvbXBvbmVudGVuLUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMSAwHgYDVQQDDBdHRU0uS09NUC1DQTEwIFRFU1QtT05MWTAeFw0yMDEwMDcwMDAwMDBaFw0yNTA4MDcwMDAwMDBaMF4xCzAJBgNVBAYTAkRFMSYwJAYDVQQKDB1nZW1hdGlrIFRFU1QtT05MWSAtIE5PVC1WQUxJRDEnMCUGA1UEAwweRVJQIFJlZmVyZW56ZW50d2lja2x1bmcgRkQgRW5jMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABKYLzjl704qFX+oEuUOyLV70i2Bn2K4jekh/YOxExtdADB3X/q7fX/tVr09GtDRxe3h1yov9TwuHaHYh91RlyMejggEUMIIBEDAMBgNVHRMBAf8EAjAAMCEGA1UdIAQaMBgwCgYIKoIUAEwEgSMwCgYIKoIUAEwEgUowHQYDVR0OBBYEFK5+wVL9g8tGve6b1MdHK1xs62H7MDgGCCsGAQUFBwEBBCwwKjAoBggrBgEFBQcwAYYcaHR0cDovL2VoY2EuZ2VtYXRpay5kZS9vY3NwLzAOBgNVHQ8BAf8EBAMCAwgwUwYFKyQIAwMESjBIMEYwRDBCMEAwMgwwRS1SZXplcHQgdmVydHJhdWVuc3fDvHJkaWdlIEF1c2bDvGhydW5nc3VtZ2VidW5nMAoGCCqCFABMBIICMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMAoGCCqGSM49BAMCA0cAMEQCIGZ20lLY2WEAGOTmNEFBB1EeU645fE0Iy2U9ypFHMlw4AiAVEP0HYut0Z8sKUk6WVanMmKXjfxO/qgQFzjsbq954dw==" - MaxLineLength:TestData.kt$TestCertificates.Vau$MIIC7jCCApWgAwIBAgIHATwrYu8gtzAKBggqhkjOPQQDAjCBhDELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxMjAwBgNVBAsMKUtvbXBvbmVudGVuLUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMSAwHgYDVQQDDBdHRU0uS09NUC1DQTEwIFRFU1QtT05MWTAeFw0yMDEwMDcwMDAwMDBaFw0yNTA4MDcwMDAwMDBaMF4xCzAJBgNVBAYTAkRFMSYwJAYDVQQKDB1nZW1hdGlrIFRFU1QtT05MWSAtIE5PVC1WQUxJRDEnMCUGA1UEAwweRVJQIFJlZmVyZW56ZW50d2lja2x1bmcgRkQgRW5jMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABKYLzjl704qFX+oEuUOyLV70i2Bn2K4jekh/YOxExtdADB3X/q7fX/tVr09GtDRxe3h1yov9TwuHaHYh91RlyMejggEUMIIBEDAMBgNVHRMBAf8EAjAAMCEGA1UdIAQaMBgwCgYIKoIUAEwEgSMwCgYIKoIUAEwEgUowHQYDVR0OBBYEFK5+wVL9g8tGve6b1MdHK1xs62H7MDgGCCsGAQUFBwEBBCwwKjAoBggrBgEFBQcwAYYcaHR0cDovL2VoY2EuZ2VtYXRpay5kZS9vY3NwLzAOBgNVHQ8BAf8EBAMCAwgwUwYFKyQIAwMESjBIMEYwRDBCMEAwMgwwRS1SZXplcHQgdmVydHJhdWVuc3fDvHJkaWdlIEF1c2bDvGhydW5nc3VtZ2VidW5nMAoGCCqCFABMBIICMB8GA1UdIwQYMBaAFCjw+OapyHfMQ0Xbmq7XOoOsDg+oMAoGCCqGSM49BAMCA0cAMEQCIGZ20lLY2WEAGOTmNEFBB1EeU645fE0Iy2U9ypFHMlw4AiAVEP0HYut0Z8sKUk6WVanMmKXjfxO/qgQFzjsbq954dw== - MaxLineLength:TestData.kt$TestCertificates.Vau$MIICsTCCAligAwIBAgIHA61I5ACUjTAKBggqhkjOPQQDAjCBhDELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxMjAwBgNVBAsMKUtvbXBvbmVudGVuLUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMSAwHgYDVQQDDBdHRU0uS09NUC1DQTEwIFRFU1QtT05MWTAeFw0yMDA4MDQwMDAwMDBaFw0yNTA4MDQyMzU5NTlaMEkxCzAJBgNVBAYTAkRFMSYwJAYDVQQKDB1nZW1hdGlrIFRFU1QtT05MWSAtIE5PVC1WQUxJRDESMBAGA1UEAwwJSURQIFNpZyAxMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABJZQrG1NWxIB3kz/6Z2zojlkJqN3vJXZ3EZnJ6JXTXw5ZDFZ5XjwWmtgfomv3VOV7qzI5ycUSJysMWDEu3mqRcajge0wgeowHQYDVR0OBBYEFJ8DVLAZWT+BlojTD4MT/Na+ES8YMDgGCCsGAQUFBwEBBCwwKjAoBggrBgEFBQcwAYYcaHR0cDovL2VoY2EuZ2VtYXRpay5kZS9vY3NwLzAMBgNVHRMBAf8EAjAAMCEGA1UdIAQaMBgwCgYIKoIUAEwEgUswCgYIKoIUAEwEgSMwHwYDVR0jBBgwFoAUKPD45qnId8xDRduartc6g6wOD6gwLQYFKyQIAwMEJDAiMCAwHjAcMBowDAwKSURQLURpZW5zdDAKBggqghQATASCBDAOBgNVHQ8BAf8EBAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgVBPhAwyX8HAVH0O0b3+VazpBAWkQNjkEVRkv+EYX1e8CIFdn4O+nivM+XVi9xiKK4dW1R7MD334OpOPTFjeEhIVV - MaxLineLength:TestData.kt$TestCertificates.Vau$MIICsTCCAligAwIBAgIHAbssqQhqOzAKBggqhkjOPQQDAjCBhDELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxMjAwBgNVBAsMKUtvbXBvbmVudGVuLUNBIGRlciBUZWxlbWF0aWtpbmZyYXN0cnVrdHVyMSAwHgYDVQQDDBdHRU0uS09NUC1DQTEwIFRFU1QtT05MWTAeFw0yMTAxMTUwMDAwMDBaFw0yNjAxMTUyMzU5NTlaMEkxCzAJBgNVBAYTAkRFMSYwJAYDVQQKDB1nZW1hdGlrIFRFU1QtT05MWSAtIE5PVC1WQUxJRDESMBAGA1UEAwwJSURQIFNpZyAzMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABIYZnwiGAn5QYOx43Z8MwaZLD3r/bz6BTcQO5pbeum6qQzYD5dDCcriw/VNPPZCQzXQPg4StWyy5OOq9TogBEmOjge0wgeowDgYDVR0PAQH/BAQDAgeAMC0GBSskCAMDBCQwIjAgMB4wHDAaMAwMCklEUC1EaWVuc3QwCgYIKoIUAEwEggQwIQYDVR0gBBowGDAKBggqghQATASBSzAKBggqghQATASBIzAfBgNVHSMEGDAWgBQo8Pjmqch3zENF25qu1zqDrA4PqDA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wHQYDVR0OBBYEFC94M9LgW44lNgoAbkPaomnLjS8/MAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDRwAwRAIgCg4yZDWmyBirgxzawz/S8DJnRFKtYU/YGNlRc7+kBHcCIBuzba3GspqSmoP1VwMeNNKNaLsgV8vMbDJb30aqaiX1 - MaxLineLength:TestData.kt$TestCertificates.Vau$MIIDGjCCAr+gAwIBAgIBFzAKBggqhkjOPQQDAjCBgTELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxNDAyBgNVBAsMK1plbnRyYWxlIFJvb3QtQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxGzAZBgNVBAMMEkdFTS5SQ0EzIFRFU1QtT05MWTAeFw0xNzA4MzAxMTM2MjJaFw0yNTA4MjgxMTM2MjFaMIGEMQswCQYDVQQGEwJERTEfMB0GA1UECgwWZ2VtYXRpayBHbWJIIE5PVC1WQUxJRDEyMDAGA1UECwwpS29tcG9uZW50ZW4tQ0EgZGVyIFRlbGVtYXRpa2luZnJhc3RydWt0dXIxIDAeBgNVBAMMF0dFTS5LT01QLUNBMTAgVEVTVC1PTkxZMFowFAYHKoZIzj0CAQYJKyQDAwIIAQEHA0IABDFinQgzfsT1CN0QWwdm7e2JiaDYHocCiy1TWpOPyHwoPC54RULeUIBJeX199Qm1FFpgeIRP1E8cjbHGNsRbju6jggEgMIIBHDAdBgNVHQ4EFgQUKPD45qnId8xDRduartc6g6wOD6gwHwYDVR0jBBgwFoAUB5AzLXVTXn/4yDe/fskmV2jfONIwQgYIKwYBBQUHAQEENjA0MDIGCCsGAQUFBzABhiZodHRwOi8vb2NzcC5yb290LWNhLnRpLWRpZW5zdGUuZGUvb2NzcDASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAVBgNVHSAEDjAMMAoGCCqCFABMBIEjMFsGA1UdEQRUMFKgUAYDVQQKoEkMR2dlbWF0aWsgR2VzZWxsc2NoYWZ0IGbDvHIgVGVsZW1hdGlrYW53ZW5kdW5nZW4gZGVyIEdlc3VuZGhlaXRza2FydGUgbWJIMAoGCCqGSM49BAMCA0kAMEYCIQCprLtIIRx1Y4mKHlNngOVAf6D7rkYSa723oRyX7J2qwgIhAKPi9GSJyYp4gMTFeZkqvj8pcAqxNR9UKV7UYBlHrdxC MaxLineLength:TestData.kt$TestCrypto$"01 754e548941e5cd073fed6d734578a484be9f0bbfa1b6fa3168ed7ffb22878f0f 9aef9bbd932a020d8828367bd080a3e72b36c41ee40c87253f9b1b0beb8371bf 257db4604af8ae0dfced37ce 86c2b491c7a8309e750b 4e6e307219863938c204dfe85502ee0a" MaxLineLength:TestResource.kt$ApduResultEnum$ACTIVATECOMMAND_APDU MaxLineLength:TestResource.kt$ParameterEnum$PARAMETER_BYTEARRAY_DEFAULT @@ -353,10 +333,10 @@ MemberNameEqualsClassName:Navigation.kt$Route$val route = arguments.fold(Uri.Builder().path(path)) { uri, param -> uri.appendQueryParameter(param.name, "{${param.name}}") }.build().toString() NestedBlockDepth:EntityUtils.kt$private suspend fun SequenceScope<Deleteable>.flatten( currentObject: Cascading, currentDepth: Int, maxDepth: Int ) NestedBlockDepth:LicenceRule.kt$LicenceRule$override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) - NestedBlockDepth:Utils.kt$ fun ByteArray.contains(other: ByteArray): Boolean + NestedBlockDepth:Utils.kt$fun ByteArray.contains(other: ByteArray): Boolean ReturnCount:BiometricPrompt.kt$private fun bestSecureOption(biometricManager: BiometricManager): Int ReturnCount:TestResource.kt$TestResource$fun getParameter(parameterEnum: ParameterEnum): Any? - ReturnCount:Utils.kt$ fun ByteArray.contains(other: ByteArray): Boolean + ReturnCount:Utils.kt$fun ByteArray.contains(other: ByteArray): Boolean SpreadOperator:AndroidStringResourceGeneratorTask.kt$AndroidStringResourceGeneratorTask$( "%L to Plurals(${tr.items.toTemplateString()}),", primaryUniqueNamesWithId.getValue(tr.name), *tr.items.toArgArray() ) SpreadOperator:AndroidStringResourceGeneratorTask.kt$AndroidStringResourceGeneratorTask$("val strings = mapOf($format)", *values) SpreadOperator:Common.kt$(id, *(args.map { AnnotatedString(it.toString()) }.toTypedArray())) @@ -387,7 +367,7 @@ TooGenericExceptionCaught:TwoDCodeScanner.kt$TwoDCodeScanner$e: Exception TooGenericExceptionCaught:TwoDCodeValidator.kt$TwoDCodeValidator$e: Exception TooGenericExceptionCaught:VauChannelInterceptor.kt$VauChannelInterceptor$e: Exception - TooManyFunctions:AppDependenciesPlugin.kt$App$App + TooManyFunctions:AppDependenciesPlugin.kt$App TooManyFunctions:Common.kt$de.gematik.ti.erp.app.utils.compose.Common.kt TooManyFunctions:DebugSettingsViewModel.kt$DebugSettingsViewModel : ViewModel TooManyFunctions:EditProfileScreen.kt$de.gematik.ti.erp.app.profiles.ui.EditProfileScreen.kt @@ -407,7 +387,7 @@ TopLevelPropertyNaming:IdpRemoteDataSource.kt$private const val defaultScope = "e-rezept openid" TopLevelPropertyNaming:IdpRemoteDataSource.kt$private const val pairingScope = "pairing openid" TopLevelPropertyNaming:PrescriptionDetailScreen.kt$private const val missingValue = "---" - UnnecessaryAbstractClass:TestDB.kt$TestDB + UnnecessaryAbstractClass:TestDB.kt$TestDB$TestDB UnusedPrivateMember:DebugScreenWrapper.kt$navigation: NavController UnusedPrivateMember:Hints.kt$innerPadding: PaddingValues UnusedPrivateMember:PrescriptionDetailScreen.kt$@Composable private fun Group( content: @Composable ColumnScope.() -> Unit ) diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 3d92606b..91ae4846 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -81,7 +81,8 @@ kotlin { compileOnly(paging("common-ktx")) } kotlinX { - coroutines("swing") + implementation(coroutines("swing")) + implementation(datetime) } dependencyInjection { compileOnly(kodein("di-framework-compose")) diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt index f891b539..6586d788 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/Main.kt @@ -80,7 +80,7 @@ import de.gematik.ti.erp.app.network.di.networkModule import de.gematik.ti.erp.app.prescription.di.prescriptionModule import de.gematik.ti.erp.app.protocol.di.protocolModule import de.gematik.ti.erp.app.utils.cleanupDbFiles -import de.gematik.ti.erp.app.vau.di.vauModule +import de.gematik.ti.erp.app.di.vauModule import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableStateFlow diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt deleted file mode 100644 index 2af23295..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EcdsaUsingShaAlgorithmExtending.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp - -import org.jose4j.jws.EcdsaUsingShaAlgorithm -import org.jose4j.jws.JsonWebSignatureAlgorithm - -open class EcdsaUsingShaAlgorithmExtending( - id: String?, - javaAlgo: String?, - curveName: String?, - signatureByteLength: Int -) : EcdsaUsingShaAlgorithm(id, javaAlgo, curveName, signatureByteLength), - JsonWebSignatureAlgorithm { - class EcdsaBP256R1UsingSha256 : EcdsaUsingShaAlgorithmExtending( - AlgorithmIdentifiersExtending.BRAINPOOL256_USING_SHA256, - "SHA256withECDSA", - EllipticCurvesExtending.BP_256, - 64 - ) - - class EcdsaBP384R1UsingSha384 : EcdsaUsingShaAlgorithmExtending( - AlgorithmIdentifiersExtending.BRAINPOOL384_USING_SHA384, - "SHA384withECDSA", - EllipticCurvesExtending.BP_384, - 64 - ) - - class EcdsaBP512R1UsingSha512 : EcdsaUsingShaAlgorithmExtending( - AlgorithmIdentifiersExtending.BRAINPOOL512_USING_SHA512, - "SHA512withECDSA", - EllipticCurvesExtending.BP_512, - 64 - ) -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt deleted file mode 100644 index ac934d00..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/EllipticCurvesExtending.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -@file:Suppress("ktlint:max-line-length", "ktlint:argument-list-wrapping") - -package de.gematik.ti.erp.app.idp - -import de.gematik.ti.erp.app.idp.EcdsaUsingShaAlgorithmExtending.EcdsaBP256R1UsingSha256 -import de.gematik.ti.erp.app.idp.EcdsaUsingShaAlgorithmExtending.EcdsaBP384R1UsingSha384 -import de.gematik.ti.erp.app.idp.EcdsaUsingShaAlgorithmExtending.EcdsaBP512R1UsingSha512 -import org.jose4j.jwa.AlgorithmFactoryFactory -import org.jose4j.keys.EllipticCurves -import java.lang.reflect.InvocationTargetException -import java.math.BigInteger -import java.security.spec.ECFieldFp -import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint -import java.security.spec.EllipticCurve - -object EllipticCurvesExtending : EllipticCurves() { - const val BP_256 = "BP-256" - const val BP_384 = "BP-384" - const val BP_512 = "BP-512" - val BP256 = ECParameterSpec( - EllipticCurve( - ECFieldFp(BigInteger("76884956397045344220809746629001649093037950200943055203735601445031516197751")), - BigInteger("56698187605326110043627228396178346077120614539475214109386828188763884139993"), - BigInteger("17577232497321838841075697789794520262950426058923084567046852300633325438902") - ), - ECPoint( - BigInteger("63243729749562333355292243550312970334778175571054726587095381623627144114786"), - BigInteger("38218615093753523893122277964030810387585405539772602581557831887485717997975") - ), - BigInteger("76884956397045344220809746629001649092737531784414529538755519063063536359079"), - 1 - ) - private val BP384 = ECParameterSpec( - EllipticCurve( - ECFieldFp(BigInteger("21659270770119316173069236842332604979796116387017648600081618503821089934025961822236561982844534088440708417973331")), - BigInteger("19048979039598244295279281525021548448223459855185222892089532512446337024935426033638342846977861914875721218402342"), - BigInteger("717131854892629093329172042053689661426642816397448020844407951239049616491589607702456460799758882466071646850065") - ), - ECPoint( - BigInteger("4480579927441533893329522230328287337018133311029754539518372936441756157459087304048546502931308754738349656551198"), - BigInteger("21354446258743982691371413536748675410974765754620216137225614281636810686961198361153695003859088327367976229294869") - ), - BigInteger("21659270770119316173069236842332604979796116387017648600075645274821611501358515537962695117368903252229601718723941"), - 1 - ) - private val BP512 = ECParameterSpec( - EllipticCurve( - ECFieldFp(BigInteger("8948962207650232551656602815159153422162609644098354511344597187200057010413552439917934304191956942765446530386427345937963894309923928536070534607816947")), - BigInteger("6294860557973063227666421306476379324074715770622746227136910445450301914281276098027990968407983962691151853678563877834221834027439718238065725844264138"), - BigInteger("3245789008328967059274849584342077916531909009637501918328323668736179176583263496463525128488282611559800773506973771797764811498834995234341530862286627") - ), - ECPoint( - BigInteger("6792059140424575174435640431269195087843153390102521881468023012732047482579853077545647446272866794936371522410774532686582484617946013928874296844351522"), - BigInteger("6592244555240112873324748381429610341312712940326266331327445066687010545415256461097707483288650216992613090185042957716318301180159234788504307628509330") - ), - BigInteger("8948962207650232551656602815159153422162609644098354511344597187200057010413418528378981730643524959857451398370029280583094215613882043973354392115544169"), - 1 - ) - private var initializedInSession = false - - @Throws( - InvocationTargetException::class, - IllegalAccessException::class, - NoSuchMethodException::class - ) - private fun addCurve(name: String, spec: ECParameterSpec) { - val method = EllipticCurves::class.java.getDeclaredMethod( - "addCurve", - String::class.java, - ECParameterSpec::class.java - ) - method.isAccessible = true - method.invoke(EllipticCurvesExtending::class.java, name, spec) - } - - fun init(): Boolean { - return if (initializedInSession) { - true - } else try { - addCurve("BP-256", BP256) - addCurve("BP-384", BP384) - addCurve("BP-512", BP512) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP256R1UsingSha256() - ) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP384R1UsingSha384() - ) - AlgorithmFactoryFactory.getInstance().jwsAlgorithmFactory.registerAlgorithm( - EcdsaBP512R1UsingSha512() - ) - initializedInSession = true - true - } catch (e: Exception) { - throw IllegalStateException("failure on init $e") - } - } -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt deleted file mode 100644 index 108c7d0d..00000000 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/JWTExtensions.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2023 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.idp - -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import org.jose4j.base64url.Base64Url -import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers -import org.jose4j.jwe.JsonWebEncryption -import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers -import org.jose4j.jws.EcdsaUsingShaAlgorithm -import org.jose4j.jws.JsonWebSignature -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey -import java.security.Signature -import javax.crypto.SecretKey - -fun buildKeyVerifier( - tokenKey: SecretKey, - codeVerifier: String, - publicEncKey: PublicKey -): JsonWebEncryption { - val payload = buildJsonObject { - put("token_key", Base64Url.encode(tokenKey.encoded)) - put("code_verifier", codeVerifier) - }.toString() - - return JsonWebEncryption().apply { - setHeader("cty", "JSON") - algorithmHeaderValue = KeyManagementAlgorithmIdentifiers.ECDH_ES - encryptionMethodHeaderParameter = ContentEncryptionAlgorithmIdentifiers.AES_256_GCM - key = publicEncKey - this.payload = payload - } -} - -private class JsonWebSignatureWithHealthCard : JsonWebSignature() { - fun encodedHeaderAndPayload(): String = - "$encodedHeader.$encodedPayload" -} - -suspend fun buildJsonWebSignatureWithHealthCard( - builder: JsonWebSignature.() -> Unit, - sign: suspend (hash: ByteArray) -> ByteArray -): String { - val jwsWithHealthCard = JsonWebSignatureWithHealthCard() - builder(jwsWithHealthCard) - - val headerAndPayload = jwsWithHealthCard.encodedHeaderAndPayload() - - jwsWithHealthCard.algorithmHeaderValue.let { - require(it.startsWith("BP")) - require(it.endsWith("R1")) - } - - val hashed = MessageDigest.getInstance("SHA-256").digest(headerAndPayload.toByteArray(Charsets.UTF_8)) - val signed = sign(hashed) - - return "$headerAndPayload.${Base64Url().base64UrlEncode(signed)}" -} - -fun buildJsonWebSignatureWithSecureElement( - builder: JsonWebSignature.() -> Unit, - privateKey: PrivateKey, - signature: Signature -): String { - val jwsWithHealthCard = JsonWebSignatureWithHealthCard() - builder(jwsWithHealthCard) - - val headerAndPayload = jwsWithHealthCard.encodedHeaderAndPayload() - - val signed = signature.apply { - initSign(privateKey) - update(headerAndPayload.toByteArray(Charsets.UTF_8)) - }.sign() - - val concatenatedSigned = EcdsaUsingShaAlgorithm.convertDerToConcatenated(signed, 64) - - return "$headerAndPayload.${Base64Url().base64UrlEncode(concatenatedSigned)}" -} diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt index dbfeea40..d7c482c9 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCase.kt @@ -19,7 +19,6 @@ package de.gematik.ti.erp.app.idp.usecase import de.gematik.ti.erp.app.BCProvider -import de.gematik.ti.erp.app.idp.EllipticCurvesExtending import de.gematik.ti.erp.app.idp.api.IdpService import de.gematik.ti.erp.app.idp.api.models.JWSPublicKey import de.gematik.ti.erp.app.idp.api.models.TokenResponse @@ -38,11 +37,11 @@ import java.security.PublicKey import java.security.SecureRandom import java.security.Security import java.security.interfaces.ECPublicKey -import java.time.Duration -import java.time.Instant import javax.crypto.SecretKey import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive @@ -58,9 +57,10 @@ import org.jose4j.jwt.NumericDate import org.jose4j.jwt.consumer.JwtContext import org.jose4j.jwt.consumer.NumericDateValidator import org.jose4j.jwx.JsonWebStructure +import kotlin.time.Duration.Companion.hours -private val discoveryDocumentMaxValidityMinutes: Int = Duration.ofHours(24).toMinutes().toInt() -private val discoveryDocumentMaxValiditySeconds: Int = Duration.ofHours(24).seconds.toInt() +private val discoveryDocumentMaxValidityMinutes: Int = 24.hours.inWholeMinutes.toInt() +private val discoveryDocumentMaxValiditySeconds: Int = 24.hours.inWholeSeconds.toInt() enum class IdpScope { Default, @@ -183,7 +183,7 @@ class IdpBasicUseCase( val config = try { repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Instant.now()) + checkIdpConfigurationValidity(it, Clock.System.now()) } } catch (e: Exception) { Napier.e("IDP config couldn't be validated", e) @@ -191,7 +191,7 @@ class IdpBasicUseCase( // retry try { repository.loadUncheckedIdpConfiguration().also { - checkIdpConfigurationValidity(it, Instant.now()) + checkIdpConfigurationValidity(it, Clock.System.now()) } } catch (e: Exception) { Napier.e("IDP config couldn't be validated again; finally aborting", e) @@ -485,7 +485,7 @@ class IdpBasicUseCase( val r = NumericDateValidator().apply { setAllowedClockSkewSeconds(60) - setEvaluationTime(NumericDate.fromMilliseconds(timestamp.toEpochMilli())) + setEvaluationTime(NumericDate.fromMilliseconds(timestamp.toEpochMilliseconds())) setRequireExp(true) setRequireIat(true) setIatAllowedSecondsInThePast(discoveryDocumentMaxValiditySeconds) diff --git a/gradle.properties b/gradle.properties index 723d3063..56977acf 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.8.0 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.9.0 GMTIK/eRezeptApp # DATA_PROTECTION_LAST_UPDATED = 2022-01-06 # diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt index b85d1100..4de34650 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/AppDependenciesPlugin.kt @@ -37,7 +37,7 @@ class AppDependenciesPlugin : Plugin { project.plugins.all { if (this is AppPlugin) { project.extensions.getByType(BaseAppModuleExtension::class).apply { - composeOptions.kotlinCompilerExtensionVersion = Dependencies.composeVersion + composeOptions.kotlinCompilerExtensionVersion = "1.3.2" buildFeatures { compose = true } @@ -69,7 +69,7 @@ class AppDependenciesPlugin : Plugin { const val TargetSdkVersion = 33 object DependencyInjection { - fun kodein(module: String) = "org.kodein.di:kodein-$module:7.14.0" + fun kodein(module: String) = "org.kodein.di:kodein-$module:7.16.0" } object Tracker @@ -89,13 +89,13 @@ class AppDependenciesPlugin : Plugin { } } object Lottie { - const val lottieVersion = "5.0.3" + const val lottieVersion = "5.2.0" const val lottie = "com.airbnb.android:lottie-compose:$lottieVersion" } object PlayServices { val location = gms("location", "21.0.1") - const val integrity = "com.google.android.play:integrity:1.0.2" - val maps = gms("maps", "18.0.1") + const val integrity = "com.google.android.play:integrity:1.1.0" + val maps = gms("maps", "18.1.0") private fun gms(module: String, version: String) = "com.google.android.gms:play-services-$module:$version" @@ -105,15 +105,15 @@ class AppDependenciesPlugin : Plugin { object Android { const val desugaring = "com.android.tools:desugar_jdk_libs:1.1.5" - const val appcompat = "androidx.appcompat:appcompat:1.4.1" + const val appcompat = "androidx.appcompat:appcompat:1.6.0" const val legacySupport = "androidx.legacy:legacy-support-v4:1.0.0" - const val coreKtx = "androidx.core:core-ktx:1.7.0" + const val coreKtx = "androidx.core:core-ktx:1.9.0" const val datastorePreferences = "androidx.datastore:datastore-preferences:1.0.0" const val biometric = "androidx.biometric:biometric:1.1.0" - const val webkit = "androidx.webkit:webkit:1.4.0" - const val security = "androidx.security:security-crypto:1.1.0-alpha03" + const val webkit = "androidx.webkit:webkit:1.6.0" + const val security = "androidx.security:security-crypto:1.1.0-alpha04" - val mapsCompose = gmaps("maps-compose", "2.7.2") + val mapsCompose = gmaps("maps-compose", "2.9.1") val maps = gmaps("maps-ktx", "3.4.0") val mapsUtils = gmaps("maps-utils-ktx", "3.4.0") val mapsAndroidUtils = gmaps("maps-utils-ktx", "2.4.0") @@ -123,38 +123,38 @@ class AppDependenciesPlugin : Plugin { fun lifecycle(module: String) = "androidx.lifecycle:lifecycle-$module:2.5.1" - const val composeNavigation = "androidx.navigation:navigation-compose:2.4.2" + const val composeNavigation = "androidx.navigation:navigation-compose:2.5.3" const val composeActivity = "androidx.activity:activity-compose:1.6.1" - const val composePaging = "androidx.paging:paging-compose:1.0.0-alpha14" + const val composePaging = "androidx.paging:paging-compose:1.0.0-alpha17" - const val cameraViewVersion = "1.2.0-rc01" - const val cameraVersion = "1.2.0-rc01" + const val cameraViewVersion = "1.2.1" + const val cameraVersion = "1.2.1" fun camera(module: String, version: String = cameraVersion) = "androidx.camera:camera-$module:$version" const val processPhoenix = "com.jakewharton:process-phoenix:2.1.2" - const val imageCropper = "com.github.CanHub:Android-Image-Cropper:4.2.1" + const val imageCropper = "com.github.CanHub:Android-Image-Cropper:4.3.2" object Test { - const val runner = "androidx.test:runner:1.5.-" + const val runner = "androidx.test:runner:1.5.2" const val orchestrator = "androidx.test:orchestrator:1.4.2" const val services = "androidx.test.services:test-services:1.4.2" const val archCore = "androidx.arch.core:core-testing:2.1.0" const val core = "androidx.test:core:1.5.0" const val rules = "androidx.test:rules:1.5.0" - const val espresso = "androidx.test.espresso:espresso-core:3.5.0" - const val espressoIntents = "androidx.test.espresso:espresso-intents:3.5.0" - const val junitExt = "androidx.test.ext:junit:1.1.4" + const val espresso = "androidx.test.espresso:espresso-core:3.5.1" + const val espressoIntents = "androidx.test.espresso:espresso-intents:3.5.1" + const val junitExt = "androidx.test.ext:junit:1.1.5" const val navigation = "androidx.navigation:navigation-testing:2.5.3" } } object AndroidX { - fun paging(suffix: String) = "androidx.paging:paging-$suffix:3.1.0" + fun paging(suffix: String) = "androidx.paging:paging-$suffix:3.1.1" } object Logging { const val napier = "io.github.aakira:napier:2.6.1" - const val slf4jNoOp = "org.slf4j:slf4j-nop:2.0.0-alpha5" + const val slf4jNoOp = "org.slf4j:slf4j-nop:2.0.6" } object Serialization { @@ -163,7 +163,7 @@ class AppDependenciesPlugin : Plugin { } object Crypto { - const val jose4j = "org.bitbucket.b_c:jose4j:0.7.12" + const val jose4j = "org.bitbucket.b_c:jose4j:0.9.2" fun bouncyCastle(provider: String, targetPlatform: String = "jdk18on") = "org.bouncycastle:$provider-$targetPlatform:1.72" @@ -180,10 +180,10 @@ class AppDependenciesPlugin : Plugin { } object Database { - const val realm = "io.realm.kotlin:library-base:1.4.0" + const val realm = "io.realm.kotlin:library-base:1.6.1" } - const val composeVersion = "1.2.1" + const val composeVersion = "1.3.+" object Compose { const val compiler = "androidx.compose.compiler:compiler:$composeVersion" @@ -199,7 +199,7 @@ class AppDependenciesPlugin : Plugin { const val materialIconsExtended = "androidx.compose.material:material-icons-extended:$composeVersion" - fun accompanist(module: String) = "com.google.accompanist:accompanist-$module:0.27.0" + fun accompanist(module: String) = "com.google.accompanist:accompanist-$module:0.28.0" object Test { const val ui = "androidx.compose.ui:ui-test:$composeVersion" @@ -212,11 +212,11 @@ class AppDependenciesPlugin : Plugin { } object ContentSquare { - const val cts = "com.contentsquare.android:library:4.12.0" + const val cts = "com.contentsquare.android:library:4.15.0" } object Test { - fun mockk(module: String) = "io.mockk:$module:1.13.2" + fun mockk(module: String) = "io.mockk:$module:1.13.3" const val junit4 = "junit:junit:4.13.2" const val snakeyaml = "org.yaml:snakeyaml:1.30" const val json = "org.json:json:20220924"