diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 567c70c9..eaeb76a5 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,11 @@ +# Release 1.17.2 +- Added Demo mode function +- Added function to change the name of scanned prescriptions +- Increased minimum SDK version to 26 and build version to 34 +- UX improvements +- Bug fixes + + # Release 1.16.1 - Added Invoice correction function for private health insurance customers - Optimized performance diff --git a/accept-licenses.exp b/accept-licenses.exp new file mode 100644 index 00000000..a1cb2a6c --- /dev/null +++ b/accept-licenses.exp @@ -0,0 +1,12 @@ +#!/usr/bin/expect -f + +set timeout -1 +spawn $env(ANDROID_SDK)/cmdline-tools/tools/bin/sdkmanager --licenses + +expect { + "Accept? (y/N): " { + send "y\r" + exp_continue + } + eof +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index 0bdb7e6d..00000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt deleted file mode 100644 index 1d8425ef..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.mini.ui - -import android.nfc.Tag -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.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.PersonOutline -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState -import de.gematik.ti.erp.app.core.IntentHandler -import de.gematik.ti.erp.app.idp.api.models.AuthenticationId -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.Avatar -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerLarge -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import java.net.URI - -class NoneEnrolledException : IllegalStateException() -class UserNotAuthenticatedException : IllegalStateException() - -@Stable -interface PromptAuthenticator { - enum class AuthResult { - Authenticated, - Cancelled, - NoneEnrolled, - UserNotAuthenticated - } - - enum class AuthScope { - Prescriptions, PairedDevices - } - - fun authenticate(profileId: ProfileIdentifier, scope: AuthScope): Flow - - suspend fun cancelAuthentication() -} - -interface AuthenticationBridge { - @Stable - sealed interface InitialAuthenticationData { - val profile: ProfilesUseCaseData.Profile - } - - data class HealthCard(val can: String, override val profile: ProfilesUseCaseData.Profile) : - InitialAuthenticationData - - data class SecureElement(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData - data class External( - val authenticatorId: String, - val authenticatorName: String, - override val profile: ProfilesUseCaseData.Profile - ) : InitialAuthenticationData - - data class None(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData - - suspend fun authenticateFor( - profileId: ProfileIdentifier - ): InitialAuthenticationData - - fun doSecureElementAuthentication( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope - ): Flow - - fun doHealthCardAuthentication( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope, - can: String, - pin: String, - tag: Tag - ): Flow - - suspend fun loadExternalAuthenticators(): List - - suspend fun doExternalAuthentication( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope, - authenticatorId: String, - authenticatorName: String - ): Result - - suspend fun doExternalAuthorization( - redirect: URI - ): Result - - suspend fun doRemoveAuthentication(profileId: ProfileIdentifier) -} - -@Stable -class Authenticator( - val authenticatorSecureElement: SecureHardwarePromptAuthenticator, - val authenticatorHealthCard: HealthCardPromptAuthenticator, - val authenticatorExternal: ExternalPromptAuthenticator, - private val bridge: AuthenticationBridge -) { - fun authenticateForPrescriptions(profileId: ProfileIdentifier): Flow = - flow { - emitAll( - when (bridge.authenticateFor(profileId)) { - is AuthenticationBridge.HealthCard -> - authenticatorHealthCard.authenticate(profileId, PromptAuthenticator.AuthScope.Prescriptions) - - is AuthenticationBridge.SecureElement -> - authenticatorSecureElement.authenticate(profileId, PromptAuthenticator.AuthScope.Prescriptions) - - is AuthenticationBridge.External -> - authenticatorExternal.authenticate(profileId, PromptAuthenticator.AuthScope.Prescriptions) - - is AuthenticationBridge.None -> flowOf(PromptAuthenticator.AuthResult.NoneEnrolled) - } - ) - } - - fun authenticateForPairedDevices(profileId: ProfileIdentifier): Flow = - flow { - emitAll( - when (bridge.authenticateFor(profileId)) { - is AuthenticationBridge.HealthCard -> - authenticatorHealthCard.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) - - is AuthenticationBridge.SecureElement -> - authenticatorSecureElement.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) - - is AuthenticationBridge.External -> - authenticatorExternal.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) - - is AuthenticationBridge.None -> flowOf(PromptAuthenticator.AuthResult.NoneEnrolled) - } - ) - } - - suspend fun cancelAllAuthentications() { - authenticatorSecureElement.cancelAuthentication() - authenticatorHealthCard.cancelAuthentication() - } -} - -@Composable -fun PromptScaffold( - title: String, - profile: ProfilesUseCaseData.Profile?, - onCancel: () -> Unit, - content: @Composable () -> Unit -) { - Surface( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - .padding(PaddingDefaults.Medium), - color = MaterialTheme.colors.surface, - shape = RoundedCornerShape(16.dp), - elevation = 8.dp - ) { - Column( - Modifier - .padding(vertical = PaddingDefaults.Medium) - ) { - Row( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - profile?.let { - Avatar( - avatarModifier = Modifier.size(36.dp), - emptyIcon = Icons.Rounded.PersonOutline, - iconModifier = Modifier.size(20.dp), - profile = profile, - ssoStatusColor = null - ) - SpacerMedium() - Column(modifier = Modifier.weight(1f)) { - Text( - title, - style = AppTheme.typography.h6, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - Text( - profile.insuranceInformation.insuranceIdentifier, - style = AppTheme.typography.body2l - ) - } - } - TextButton(onClick = onCancel) { - Text(stringResource(R.string.cdw_nfc_dlg_cancel)) - } - } - SpacerLarge() - content() - } - } -} - -@Composable -fun rememberAuthenticator(intentHandler: IntentHandler): Authenticator { - val bridge = rememberMiniCardWallController() - val promptSE = rememberSecureHardwarePromptAuthenticator(bridge) - val promptHC = rememberHealthCardPromptAuthenticator(bridge) - val promptEX = rememberExternalPromptAuthenticator(bridge, intentHandler) - return remember { - Authenticator( - authenticatorSecureElement = promptSE, - authenticatorHealthCard = promptHC, - authenticatorExternal = promptEX, - bridge = bridge - ) - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt b/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt deleted file mode 100644 index 3f2bca82..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt +++ /dev/null @@ -1,638 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.cardwall.mini.ui - -import android.content.Intent -import android.provider.Settings -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -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.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.systemBarsPadding -import de.gematik.ti.erp.app.MainActivity -import de.gematik.ti.erp.app.NfcNotEnabledException -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.cardwall.ui.ReadingCardAnimation -import de.gematik.ti.erp.app.cardwall.ui.SearchingCardAnimation -import de.gematik.ti.erp.app.cardwall.ui.TagLostCard -import de.gematik.ti.erp.app.cardwall.ui.pinRetriesLeft -import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.Dialog -import de.gematik.ti.erp.app.utils.compose.PrimaryButton -import de.gematik.ti.erp.app.utils.compose.toAnnotatedString -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@Stable -class HealthCardPromptAuthenticator( - val activity: MainActivity, - private val bridge: AuthenticationBridge -) : PromptAuthenticator { - private sealed interface Request { - class CredentialsEntered(val pin: String) : Request - object Cancel : Request - } - - private val requestChannel = Channel(Channel.RENDEZVOUS) - - internal sealed interface State { - object None : State - object EnterCredentials : State - - sealed interface ReadState : State { - object Searching : ReadState - - sealed interface Reading : ReadState { - object Reading00 : Reading - object Reading25 : Reading - object Reading50 : Reading - object Reading75 : Reading - object Success : Reading - } - - sealed interface Error : ReadState { - object NfcDisabled : Error - object TagLost : Error - object RemoteCommunicationFailed : Error - object CardAccessNumberWrong : Error - class PersonalIdentificationWrong(val retriesLeft: Int) : Error - object HealthCardBlocked : Error - object RemoteCommunicationInvalidCertificate : Error - object RemoteCommunicationInvalidOCSP : Error - } - } - } - - internal var state by mutableStateOf(State.None) - private set - - var profile by mutableStateOf(null) - private set - - private val tagFlow = activity.nfcTagFlow - .filter { - // only let interrupted communications through - !(state is State.ReadState.Error && state!=State.ReadState.Error.TagLost) - } - - override fun authenticate( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope - ): Flow = channelFlow { - val requestChannelFlow = requestChannel.receiveAsFlow() - - when (val authFor = bridge.authenticateFor(profileId)) { - is AuthenticationBridge.HealthCard -> { - state = State.EnterCredentials - profile = authFor.profile - - requestChannelFlow.collectLatest { req -> - when (req) { - Request.Cancel -> { - send(PromptAuthenticator.AuthResult.Cancelled) - cancel() - } - - is Request.CredentialsEntered -> { - state = State.ReadState.Searching - - tagFlow - .catch { - if (it is NfcNotEnabledException) { - state = State.ReadState.Error.NfcDisabled - } - } - .collectLatest { tag -> - bridge.doHealthCardAuthentication( - profileId = profileId, - scope = scope, - can = authFor.can, - pin = req.pin, - tag = tag - ).collect { - it.emitAuthState() - if (it.isFinal()) { - send(PromptAuthenticator.AuthResult.Authenticated) - cancel() - } - } - } - } - } - } - } - - else -> { - send(PromptAuthenticator.AuthResult.Cancelled) - } - } - }.onCompletion { - state = State.None - profile = null - } - - @Requirement( - "A_19937", - "A_20079", - "A_20605#1", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Propagates IDP auth states to the user." - ) - @Requirement( - "GS-A_5542#1", - sourceSpecification = "gemSpec_Krypt", - rationale = "Propagates IDP auth states to the user." - ) - private fun AuthenticationState.emitAuthState() { - when { - isInProgress() -> { - when (this) { - AuthenticationState.HealthCardCommunicationChannelReady -> - state = State.ReadState.Reading.Reading00 - - AuthenticationState.HealthCardCommunicationTrustedChannelEstablished -> - state = State.ReadState.Reading.Reading25 - - AuthenticationState.HealthCardCommunicationFinished -> - state = State.ReadState.Reading.Reading50 - - AuthenticationState.IDPCommunicationFinished -> - state = State.ReadState.Reading.Reading75 - - else -> {} - } - } - - isFailure() -> { - state = when (this) { - AuthenticationState.HealthCardCommunicationInterrupted -> - State.ReadState.Error.TagLost - - AuthenticationState.HealthCardCardAccessNumberWrong -> - State.ReadState.Error.CardAccessNumberWrong - - AuthenticationState.HealthCardPin2RetriesLeft -> - State.ReadState.Error.PersonalIdentificationWrong(2) - - AuthenticationState.HealthCardPin1RetryLeft -> - State.ReadState.Error.PersonalIdentificationWrong(1) - - AuthenticationState.HealthCardBlocked -> - State.ReadState.Error.HealthCardBlocked - - AuthenticationState.IDPCommunicationFailed -> - State.ReadState.Error.RemoteCommunicationFailed - - AuthenticationState.IDPCommunicationInvalidCertificate -> - State.ReadState.Error.RemoteCommunicationInvalidCertificate - - AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> - State.ReadState.Error.RemoteCommunicationInvalidOCSP - - else -> - State.ReadState.Error.TagLost - } - } - } - } - - override suspend fun cancelAuthentication() { - requestChannel.trySend(Request.Cancel) - } - - internal suspend fun onCancel() { - requestChannel.send(Request.Cancel) - } - - internal suspend fun onCredentialsEntered(pin: String) { - requestChannel.send(Request.CredentialsEntered(pin)) - } -} - -@Composable -fun rememberHealthCardPromptAuthenticator( - bridge: AuthenticationBridge -): HealthCardPromptAuthenticator { - val activity = LocalContext.current as MainActivity - return remember { - HealthCardPromptAuthenticator(activity, bridge) - } -} - -@Composable -fun HealthCardPrompt( - authenticator: HealthCardPromptAuthenticator -) { - val scope = rememberCoroutineScope() - val state = authenticator.state - val profile = authenticator.profile - - val isError = state is HealthCardPromptAuthenticator.State.ReadState.Error - val isTagLost = state is HealthCardPromptAuthenticator.State.ReadState.Error.TagLost - - if (state != HealthCardPromptAuthenticator.State.None && (!isError || isTagLost)) { - Dialog( - onDismissRequest = {}, - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) - ) { - Box( - Modifier - .semantics(false) { } - .fillMaxSize() - .background(SolidColor(Color.Black), alpha = 0.5f) - .verticalScroll(rememberScrollState()) - .imePadding() - .systemBarsPadding(), - contentAlignment = Alignment.BottomCenter - ) { - PromptScaffold( - title = stringResource(R.string.mini_cdw_title), - profile = profile, - onCancel = { - scope.launch { - authenticator.onCancel() - } - } - ) { - when (state) { - HealthCardPromptAuthenticator.State.EnterCredentials -> - HealthCardCredentials( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - onNext = { - scope.launch { - authenticator.onCredentialsEntered(it) - } - } - ) - - is HealthCardPromptAuthenticator.State.ReadState -> - HealthCardAnimation( - modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), - state = state - ) - - else -> {} - } - } - } - } - } - if (isError) { - HealthCardErrorDialog( - state = state as HealthCardPromptAuthenticator.State.ReadState.Error, - onCancel = { - scope.launch { - authenticator.onCancel() - } - }, - onEnableNfc = { - scope.launch(Dispatchers.Main) { - authenticator.activity.startActivity( - Intent(Settings.ACTION_NFC_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ) - authenticator.onCancel() - } - } - ) - } -} - -private val PinRegex = """^\d{0,8}$""".toRegex() -private val PinCorrectRegex = """^\d{6,8}$""".toRegex() - -@Composable -private fun HealthCardCredentials( - modifier: Modifier, - onNext: (pin: String) -> Unit -) { - var pin by remember { mutableStateOf("") } - var pinVisible by remember { mutableStateOf(false) } - val pinCorrect by remember { - derivedStateOf { pin.matches(PinCorrectRegex) } - } - - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) - ) { - Text( - stringResource(R.string.mini_cdw_intro_description), - style = AppTheme.typography.body2l - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = pin, - onValueChange = { - if (it.matches(PinRegex)) { - pin = it - } - }, - label = { Text(stringResource(R.string.mini_cdw_pin_input_label)) }, - placeholder = { Text(stringResource(R.string.mini_cdw_pin_input_placeholder)) }, - visualTransformation = if (pinVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.NumberPassword - ), - shape = RoundedCornerShape(8.dp), - colors = TextFieldDefaults.outlinedTextFieldColors( - unfocusedLabelColor = AppTheme.colors.neutral400, - placeholderColor = AppTheme.colors.neutral400, - trailingIconColor = AppTheme.colors.neutral400 - ), - keyboardActions = KeyboardActions { - onNext(pin) - }, - trailingIcon = { - IconToggleButton( - checked = pinVisible, - onCheckedChange = { pinVisible = it } - ) { - Icon( - if (pinVisible) { - Icons.Rounded.Visibility - } else { - Icons.Rounded.VisibilityOff - }, - null - ) - } - } - ) - PrimaryButton( - onClick = { onNext(pin) }, - enabled = pinCorrect, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.mini_cdw_pin_next)) - } - } -} - -private const val InfoTextRoundTime = 5000L - -@Composable -private fun HealthCardAnimation( - modifier: Modifier, - state: HealthCardPromptAuthenticator.State.ReadState -) { - Column( - modifier = modifier - .padding(PaddingDefaults.Large) - .wrapContentSize() - .testTag("cdw_auth_nfc_bottom_sheet"), - verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .defaultMinSize(minHeight = 150.dp) - .fillMaxWidth() - ) { - when (state) { - HealthCardPromptAuthenticator.State.ReadState.Searching -> SearchingCardAnimation() - is HealthCardPromptAuthenticator.State.ReadState.Reading -> ReadingCardAnimation() - is HealthCardPromptAuthenticator.State.ReadState.Error -> TagLostCard() - } - } - - // how to hold your card - val rotatingScanCardAssistance = listOf( - Pair( - stringResource(R.string.cdw_nfc_search1_headline), - stringResource(R.string.cdw_nfc_search1_info) - ), - Pair( - stringResource(R.string.cdw_nfc_search2_headline), - stringResource(R.string.cdw_nfc_search2_info) - ), - Pair( - stringResource(R.string.cdw_nfc_search3_headline), - stringResource(R.string.cdw_nfc_search3_info) - ) - ) - - var info by remember { mutableStateOf(rotatingScanCardAssistance.first()) } - - LaunchedEffect(Unit) { - while (true) { - snapshotFlow { state } - .first { - state is HealthCardPromptAuthenticator.State.ReadState.Searching - } - - var i = 0 - while (state is HealthCardPromptAuthenticator.State.ReadState.Searching) { - info = rotatingScanCardAssistance[i] - - i = if (i < rotatingScanCardAssistance.size - 1) { - i + 1 - } else { - 0 - } - - delay(InfoTextRoundTime) - } - } - } - - info = when (state) { - HealthCardPromptAuthenticator.State.ReadState.Reading.Reading00 -> Pair( - stringResource(R.string.cdw_nfc_found_headline), - stringResource(R.string.cdw_nfc_found_info) - ) - - HealthCardPromptAuthenticator.State.ReadState.Reading.Reading25 -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_trusted_channel_established), - stringResource(R.string.cdw_nfc_communication_info) - ) - - HealthCardPromptAuthenticator.State.ReadState.Reading.Reading50 -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_certificate_loaded), - stringResource(R.string.cdw_nfc_communication_info) - ) - - HealthCardPromptAuthenticator.State.ReadState.Reading.Reading75 -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_pin_verified), - stringResource(R.string.cdw_nfc_communication_info) - ) - - HealthCardPromptAuthenticator.State.ReadState.Reading.Success -> Pair( - stringResource(R.string.cdw_nfc_communication_headline_challenge_signed), - stringResource(R.string.cdw_nfc_communication_info) - ) - - HealthCardPromptAuthenticator.State.ReadState.Error.TagLost -> Pair( - stringResource(R.string.cdw_nfc_tag_lost_headline), - stringResource(R.string.cdw_nfc_tag_lost_info) - ) - - else -> info - } - - Text( - info.first, - style = AppTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Text( - info.second, - style = AppTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } -} - -@Requirement( - "A_20079", - "A_20085", - "A_20605#2", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Display error messages from endpoint." -) -@Composable -private fun HealthCardErrorDialog( - state: HealthCardPromptAuthenticator.State.ReadState.Error, - onCancel: () -> Unit, - onEnableNfc: () -> Unit -) { - if (state == HealthCardPromptAuthenticator.State.ReadState.Error.NfcDisabled) { - CommonAlertDialog( - header = stringResource(R.string.cdw_enable_nfc_header), - info = stringResource(R.string.cdw_enable_nfc_info), - cancelText = stringResource(R.string.cancel), - actionText = stringResource(R.string.cdw_enable_nfc_btn_text), - onCancel = onCancel, - onClickAction = onEnableNfc - ) - } else { - val retryText = when (state) { - HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationFailed -> Pair( - stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), - stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() - ) - - HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationInvalidCertificate -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() - ) - - HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationInvalidOCSP -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString() - ) - - HealthCardPromptAuthenticator.State.ReadState.Error.CardAccessNumberWrong -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error_alert).toAnnotatedString(), - stringResource(R.string.cdw_nfc_intro_step2_info_on_can_error).toAnnotatedString() - ) - - is HealthCardPromptAuthenticator.State.ReadState.Error.PersonalIdentificationWrong -> Pair( - stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error_alert).toAnnotatedString(), - pinRetriesLeft(state.retriesLeft) - ) - - HealthCardPromptAuthenticator.State.ReadState.Error.HealthCardBlocked -> Pair( - stringResource(R.string.cdw_header_on_card_blocked).toAnnotatedString(), - stringResource(R.string.cdw_info_on_card_blocked).toAnnotatedString() - ) - - else -> null - } - - retryText?.let { (title, message) -> - - AcceptDialog( - header = title, - info = message, - acceptText = stringResource(R.string.ok), - onClickAccept = onCancel - ) - } - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt b/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt deleted file mode 100644 index db7ff605..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderState.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import androidx.annotation.VisibleForTesting -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.core.complexAutoSaver -import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.kodein.di.compose.rememberInstance - -@Stable -class PharmacyOrderState( - val profile: ProfilesUseCaseData.Profile, - private val useCase: PharmacySearchUseCase, - private val scope: CoroutineScope -) { - - var selectedPharmacy: PharmacyUseCaseData.Pharmacy? by mutableStateOf(null) - private set - - var selectedOrderOption: PharmacyScreenData.OrderOption? by mutableStateOf(null) - private set - - fun onSelectPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy, orderOption: PharmacyScreenData.OrderOption) { - selectedPharmacy = pharmacy - selectedOrderOption = orderOption - } - - private var unSelectedPrescriptions: MutableStateFlow> = MutableStateFlow(emptyList()) - - fun onSelectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { - unSelectedPrescriptions.update { - it - order.taskId - } - } - - fun onDeselectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { - unSelectedPrescriptions.update { - it + order.taskId - } - } - - private val prescriptionOrderFlow = - useCase - .prescriptionDetailsForOrdering(profile.id) - .shareIn(scope, SharingStarted.Lazily, 1) - - private val hasRedeemableTasksFlow = - prescriptionOrderFlow.map { it.prescriptions.isNotEmpty() } - - val hasRedeemableTasks - @Composable - get() = hasRedeemableTasksFlow.collectAsState(false) - - @VisibleForTesting - val orderFlow = - combine( - unSelectedPrescriptions, - prescriptionOrderFlow - ) { unSelectedPrescriptions, prescriptionOrder -> - prescriptionOrder.copy( - prescriptions = prescriptionOrder.prescriptions.filter { - it.taskId !in unSelectedPrescriptions - } - ) - } - - val order - @Composable - get() = orderFlow.collectAsState(PharmacyUseCaseData.OrderState.Empty) - - private val prescriptionFlow = - prescriptionOrderFlow.map { - it.prescriptions - } - - val prescriptions - @Composable - get() = prescriptionFlow.collectAsState(emptyList()) - - fun onSaveContact(contact: PharmacyUseCaseData.ShippingContact) { - scope.launch { - useCase.saveShippingContact(contact) - } - } - - fun onResetPharmacySelection() { - selectedPharmacy = null - selectedOrderOption = null - } - - fun onResetPrescriptionSelection() { - unSelectedPrescriptions.value = emptyList() - } -} - -@Composable -fun rememberPharmacyOrderState(): PharmacyOrderState { - val activeProfile = LocalProfileHandler.current.activeProfile - val useCase by rememberInstance() - val dispatchProvider by rememberInstance() - return rememberSaveable( - activeProfile, - saver = complexAutoSaver() - ) { - PharmacyOrderState( - activeProfile, - useCase, - CoroutineScope(dispatchProvider.Default) - ) - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt deleted file mode 100644 index 841c280b..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt +++ /dev/null @@ -1,927 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT 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:OptIn(ExperimentalMaterialApi::class) - -package de.gematik.ti.erp.app.prescription.detail.ui - -import android.net.Uri -import androidx.compose.foundation.MutatorMutex -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.SnackbarHost -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material.rememberModalBottomSheetState -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.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.analytics.trackPrescriptionDetailPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry -import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData.scannedPrescriptionIndex -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsNavigationScreens -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.ui.DirectAssignmentChip -import de.gematik.ti.erp.app.prescription.ui.FailureDetailsStatusChip -import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState -import de.gematik.ti.erp.app.prescription.ui.PrescriptionStateInfo -import de.gematik.ti.erp.app.prescription.ui.ScannedChip -import de.gematik.ti.erp.app.prescription.ui.SentStatusChip -import de.gematik.ti.erp.app.prescription.ui.SubstitutionAllowedChip -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold -import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog -import de.gematik.ti.erp.app.utils.compose.HealthPortalLink -import de.gematik.ti.erp.app.utils.compose.Label -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall -import de.gematik.ti.erp.app.utils.compose.PrimaryButtonTiny -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium -import de.gematik.ti.erp.app.utils.compose.SpacerXLarge -import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString -import de.gematik.ti.erp.app.utils.compose.handleIntent -import de.gematik.ti.erp.app.utils.compose.provideEmailIntent -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -const val MissingValue = "---" - -@Composable -fun PrescriptionDetailsScreen( - taskId: String, - mainNavController: NavController -) { - val prescriptionDetailsController = rememberPrescriptionDetailsController() - - val prescription by produceState(null) { - prescriptionDetailsController.prescriptionDetailsFlow(taskId).collect { - value = it - } - } - - var selectedMedication: PrescriptionData.Medication? by remember { mutableStateOf(null) } - var selectedIngredient: SyncedTaskData.Ingredient? by remember { mutableStateOf(null) } - - val mainScope = rememberCoroutineScope { Dispatchers.Main } - val onBack: () -> Unit = { - mainScope.launch { - mainNavController.popBackStack() // TODO onBack instead of NavController - } - } - val navController = rememberNavController() - var previousNavEntry by remember { mutableStateOf("prescriptionDetail") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - prescription?.let { pres -> - NavHost( - navController = navController, - startDestination = PrescriptionDetailsNavigationScreens.Overview.route - ) { - composable(PrescriptionDetailsNavigationScreens.Overview.route) { - PrescriptionDetailsWithScaffold( - prescription = pres, - prescriptionDetailsController = prescriptionDetailsController, - navController = navController, - onClickMedication = { - selectedMedication = it - navController.navigate(PrescriptionDetailsNavigationScreens.Medication.path()) - }, - onBack = onBack - ) - } - composable(PrescriptionDetailsNavigationScreens.MedicationOverview.route) { - MedicationOverviewScreen( - prescription = pres as PrescriptionData.Synced, - onClickMedication = { - selectedMedication = it - navController.navigate(PrescriptionDetailsNavigationScreens.Medication.path()) - }, - onBack = onBack - ) - } - composable(PrescriptionDetailsNavigationScreens.Medication.route) { - SyncedMedicationDetailScreen( - prescription = pres as PrescriptionData.Synced, - medication = requireNotNull(selectedMedication), - onClickIngredient = { - selectedIngredient = it - navController.navigate(PrescriptionDetailsNavigationScreens.Ingredient.path()) - }, - onBack = { - navController.popBackStack() - } - ) - } - composable(PrescriptionDetailsNavigationScreens.Ingredient.route) { - IngredientScreen( - ingredient = requireNotNull(selectedIngredient), - onBack = { - navController.popBackStack() - } - ) - } - composable(PrescriptionDetailsNavigationScreens.Patient.route) { - PatientScreen( - prescription = pres as PrescriptionData.Synced, - onBack = { - navController.popBackStack() - } - ) - } - composable(PrescriptionDetailsNavigationScreens.Prescriber.route) { - PrescriberScreen( - prescription = pres as PrescriptionData.Synced, - onBack = { - navController.popBackStack() - } - ) - } - composable(PrescriptionDetailsNavigationScreens.Accident.route) { - AccidentInformation( - prescription = pres as PrescriptionData.Synced, - onBack = { navController.popBackStack() } - ) - } - composable(PrescriptionDetailsNavigationScreens.Organization.route) { - OrganizationScreen( - prescription = pres as PrescriptionData.Synced, - onBack = { navController.popBackStack() } - ) - } - composable(PrescriptionDetailsNavigationScreens.TechnicalInformation.route) { - TechnicalInformation( - prescription = pres, - onBack = { navController.popBackStack() } - ) - } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun PrescriptionDetailsWithScaffold( - prescription: PrescriptionData.Prescription, - prescriptionDetailsController: PrescriptionDetailsController, - navController: NavHostController, - onClickMedication: (PrescriptionData.Medication) -> Unit, - onBack: () -> Unit -) { - val scaffoldState = rememberScaffoldState() - val listState = rememberLazyListState() - // val shareHandler = rememberSharePrescriptionController() - - val sheetState = rememberModalBottomSheetState( - ModalBottomSheetValue.Hidden, - confirmStateChange = { it != ModalBottomSheetValue.HalfExpanded } - ) - - val coroutineScope = rememberCoroutineScope() - - var infoBottomSheetContent: PrescriptionDetailBottomSheetContent? by remember { mutableStateOf(null) } - - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - infoBottomSheetContent?.let { analytics.trackPrescriptionDetailPopUps(it) } - } else { - analytics.onPopUpClosed() - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - LaunchedEffect(infoBottomSheetContent) { - if (infoBottomSheetContent != null) { - sheetState.show() - } else { - sheetState.hide() - } - } - ModalBottomSheetLayout( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.Screen), - sheetState = sheetState, - sheetContent = { - Box( - Modifier - .heightIn(min = 56.dp) - .navigationBarsPadding() - ) { - infoBottomSheetContent?.let { - PrescriptionDetailInfoSheetContent(infoContent = it) - } - } - }, - sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) - ) { - AnimatedElevationScaffold( - scaffoldState = scaffoldState, - listState = listState, - onBack = onBack, - topBarTitle = stringResource(R.string.prescription_details), - navigationMode = NavigationBarMode.Close, - snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, - actions = { - // if (prescription.accessCode != null) { - // IconButton(onClick = { - // shareHandler.share(taskId = prescription.taskId, prescription.accessCode!!) - // }) { - // Icon(Icons.Rounded.Share, null, tint = AppTheme.colors.primary700) - // } - // } - - val context = LocalContext.current - val authenticator = LocalAuthenticator.current - val deletePrescriptionsHandle = remember { - DeletePrescriptions( - prescriptionDetailsController = prescriptionDetailsController, - authenticator = authenticator - ) - } - - DeleteAction(prescription) { - val deleteState = deletePrescriptionsHandle.deletePrescription( - profileId = prescription.profileId, - taskId = prescription.taskId - ) - - when (deleteState) { - is PrescriptionServiceErrorState -> { - coroutineScope.launch { - deleteErrorMessage(context, deleteState)?.let { - scaffoldState.snackbarHostState.showSnackbar(it) - } - } - } - - is DeletePrescriptions.State.Deleted -> onBack() - } - } - } - ) { innerPadding -> - when (prescription) { - is PrescriptionData.Synced -> - SyncedPrescriptionOverview( - navController = navController, - listState = listState, - prescription = prescription, - onSelectMedication = onClickMedication, - onShowInfo = { - infoBottomSheetContent = it - coroutineScope.launch { - sheetState.show() - } - } - ) - - is PrescriptionData.Scanned -> - ScannedPrescriptionOverview( - navController = navController, - listState = listState, - prescription = prescription, - onSwitchRedeemed = { - coroutineScope.launch { - prescriptionDetailsController.redeemScannedTask( - taskId = prescription.taskId, - redeem = it - ) - } - }, - onShowInfo = { - infoBottomSheetContent = it - coroutineScope.launch { - sheetState.show() - } - } - ) - } - } - } -} - -@Composable -private fun DeleteAction( - prescription: PrescriptionData.Prescription, - onClickDelete: suspend () -> Unit -) { - var showDeletePrescriptionDialog by remember { mutableStateOf(false) } - var deletionInProgress by remember { mutableStateOf(false) } - - val coroutineScope = rememberCoroutineScope() - val mutex = MutatorMutex() - - var dropdownExpanded by remember { mutableStateOf(false) } - - val isDeletable by remember { - derivedStateOf { - (prescription as? PrescriptionData.Synced)?.isDeletable ?: true - } - } - - IconButton( - onClick = { dropdownExpanded = true }, - modifier = Modifier.testTag(TestTag.Prescriptions.Details.MoreButton) - ) { - Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) - } - DropdownMenu( - expanded = dropdownExpanded, - onDismissRequest = { dropdownExpanded = false }, - offset = DpOffset(24.dp, 0.dp) - ) { - DropdownMenuItem( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.DeleteButton), - enabled = isDeletable, - onClick = { - dropdownExpanded = false - showDeletePrescriptionDialog = true - } - ) { - Text( - text = stringResource(R.string.pres_detail_dropdown_delete), - color = if (isDeletable) { - AppTheme.colors.red600 - } else { - AppTheme.colors.neutral400 - } - ) - } - } - - if (showDeletePrescriptionDialog) { - val info = stringResource(R.string.pres_detail_delete_msg) - val cancelText = stringResource(R.string.pres_detail_delete_no) - val actionText = stringResource(R.string.pres_detail_delete_yes) - - CommonAlertDialog( - header = null, - info = info, - cancelText = cancelText, - actionText = actionText, - enabled = !deletionInProgress, - onCancel = { - showDeletePrescriptionDialog = false - }, - onClickAction = { - coroutineScope.launch { - mutex.mutate { - try { - deletionInProgress = true - onClickDelete() - } finally { - showDeletePrescriptionDialog = false - deletionInProgress = false - } - } - } - } - ) - } -} - -@Composable -private fun SyncedPrescriptionOverview( - navController: NavController, - listState: LazyListState, - prescription: PrescriptionData.Synced, - onSelectMedication: (PrescriptionData.Medication) -> Unit, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -) { - val noValueText = stringResource(R.string.pres_details_no_value) - - Column { - val colPadding = if (prescription.isIncomplete) { - PaddingValues() - } else { - WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - } - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .testTag(TestTag.Prescriptions.Details.Content), - contentPadding = colPadding - ) { - item { - SyncedHeader( - prescription = prescription, - onShowInfo = onShowInfo - ) - } - - item { - val text = additionalFeeText(prescription.medicationRequest.additionalFee) ?: noValueText - - Label( - text = text, - label = stringResource(R.string.pres_details_additional_fee), - onClick = onClickAdditionalFee( - prescription.medicationRequest.additionalFee, - onShowInfo - ) - ) - } - - prescription.medicationRequest.emergencyFee?.let { emergencyFee -> - item { - val text = emergencyFeeText(emergencyFee) - Label( - text = text, - label = stringResource(R.string.pres_details_emergency_fee), - onClick = onClickEmergencyFee(emergencyFee, onShowInfo) - ) - } - } - - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.MedicationButton), - text = prescription.name ?: noValueText, - label = stringResource(R.string.pres_details_medication), - onClick = onClickMedication(prescription, onSelectMedication, navController) - ) - } - - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.PatientButton), - text = prescription.patient.name ?: noValueText, - label = stringResource(R.string.pres_detail_patient_header), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.Patient.path()) - } - ) - } - - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriberButton), - text = prescription.practitioner.name ?: noValueText, - label = stringResource(R.string.pres_detail_practitioner_header), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.Prescriber.path()) - } - ) - } - - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.OrganizationButton), - text = prescription.organization.name ?: noValueText, - label = stringResource(R.string.pres_detail_organization_header), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.Organization.path()) - } - ) - } - - item { - Label( - text = stringResource(R.string.pres_detail_accident_header), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.Accident.path()) - } - ) - } - - item { - Label( - modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformationButton), - text = stringResource(R.string.pres_detail_technical_information), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.TechnicalInformation.path()) - } - ) - } - - item { - HealthPortalLink( - Modifier.padding( - horizontal = PaddingDefaults.Medium, - vertical = PaddingDefaults.XXLarge - ) - ) - } - } - - if (prescription.isIncomplete) { - FailureBanner( - Modifier - .fillMaxWidth() - .navigationBarsPadding(), - prescription - ) - } - } -} - -@Composable -private fun onClickMedication( - prescription: PrescriptionData.Synced, - onSelectMedication: (PrescriptionData.Medication) -> Unit, - navController: NavController -): () -> Unit = { - if (!prescription.isDispensed) { - onSelectMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) - } else { - navController.navigate(PrescriptionDetailsNavigationScreens.MedicationOverview.path()) - } -} - -@Composable -private fun onClickEmergencyFee( - emergencyFee: Boolean, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -): () -> Unit = { - if (emergencyFee) { - onShowInfo( - PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt() - ) - } else { - onShowInfo( - PrescriptionDetailBottomSheetContent.EmergencyFee() - ) - } -} - -@Composable -private fun emergencyFeeText(emergencyFee: Boolean) = if (emergencyFee) { - // false - emergencyFee fee is to be paid by the insured (default value) - // true - emergencyFee fee is not to be paid by the insured but by the payer - stringResource(R.string.pres_detail_noctu_no) -} else { - stringResource(R.string.pres_detail_noctu_yes) -} - -@Composable -private fun onClickAdditionalFee( - additionalFee: SyncedTaskData.AdditionalFee, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -): () -> Unit = { - when (additionalFee) { - SyncedTaskData.AdditionalFee.NotExempt -> { - onShowInfo( - PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt() - ) - } - SyncedTaskData.AdditionalFee.Exempt -> { - onShowInfo( - PrescriptionDetailBottomSheetContent.AdditionalFeeExempt() - ) - } - else -> {} - } -} - -@Composable -private fun additionalFeeText(additionalFee: SyncedTaskData.AdditionalFee): String? = when (additionalFee) { - SyncedTaskData.AdditionalFee.Exempt -> - stringResource(R.string.pres_detail_no) - SyncedTaskData.AdditionalFee.NotExempt -> - stringResource(R.string.pres_detail_yes) - else -> null -} - -@Composable -private fun FailureBanner( - modifier: Modifier, - prescription: PrescriptionData.Synced -) { - val mailAddress = stringResource(R.string.settings_contact_mail_address) - val subject = stringResource(R.string.settings_feedback_mail_subject) - - val context = LocalContext.current - Row( - modifier - .fillMaxWidth() - .background(AppTheme.colors.neutral050) - .padding(PaddingDefaults.Medium), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - stringResource(R.string.prescription_failure_info), - style = AppTheme.typography.body2, - modifier = Modifier.weight(1f) - ) - SpacerMedium() - PrimaryButtonTiny( - onClick = { - val body = """ - PVS ID: ${prescription.task.pvsIdentifier} - - ${prescription.failureToReport} - """.trimIndent() - - context.handleIntent( - provideEmailIntent( - address = mailAddress, - body = body, - subject = subject - ) - ) - }, - colors = ButtonDefaults.buttonColors( - backgroundColor = AppTheme.colors.red600, - contentColor = AppTheme.colors.neutral000 - ) - ) { - Text(stringResource(R.string.report_prescription_failure)) - } - } -} - -@Composable -fun SyncedHeader( - prescription: PrescriptionData.Synced, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -) { - Column( - Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - prescription.name ?: stringResource(R.string.prescription_medication_default_name), - style = AppTheme.typography.h5, - textAlign = TextAlign.Center, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - when { - prescription.isIncomplete -> { - SpacerShortMedium() - FailureDetailsStatusChip( - onClick = { - onShowInfo(PrescriptionDetailBottomSheetContent.Failure()) - } - ) - } - - prescription.isDirectAssignment -> { - SpacerShortMedium() - DirectAssignmentChip( - onClick = { - onShowInfo( - PrescriptionDetailBottomSheetContent.DirectAssignment() - ) - } - ) - } - - prescription.isSubstitutionAllowed -> { - SpacerShortMedium() - SubstitutionAllowedChip( - onClick = { - onShowInfo( - PrescriptionDetailBottomSheetContent.SubstitutionAllowed() - ) - } - ) - } - } - - SpacerShortMedium() - - val onClick = when { - !prescription.isDirectAssignment && - ( - prescription.state is SyncedTaskData.SyncedTask.Ready || - prescription.state is SyncedTaskData.SyncedTask.LaterRedeemable - ) -> { - { - onShowInfo( - PrescriptionDetailBottomSheetContent.HowLongValid( - prescription - ) - ) - } - } - - else -> null - } - SyncedStatus( - prescription = prescription, - onClick = onClick - ) - SpacerXLarge() - } -} - -@Composable -fun SyncedStatus( - modifier: Modifier = Modifier, - prescription: PrescriptionData.Synced, - onClick: (() -> Unit)? = null -) { - val clickableModifier = if (onClick != null) { - Modifier - .clickable(role = Role.Button, onClick = onClick) - .padding(start = PaddingDefaults.Tiny) - } else { - Modifier - } - - Row( - modifier = modifier - .then(clickableModifier), - verticalAlignment = Alignment.CenterVertically - ) { - if (prescription.isDirectAssignment) { - val text = if (prescription.isDispensed) { - stringResource(R.string.pres_details_direct_assignment_received_state) - } else { - stringResource(R.string.pres_details_direct_assignment_state) - } - Text( - text, - style = AppTheme.typography.body2l, - textAlign = TextAlign.Center - ) - } else { - PrescriptionStateInfo(prescription.state, textAlign = TextAlign.Center) - } - if (onClick != null) { - Spacer(Modifier.padding(2.dp)) - Icon( - Icons.Rounded.KeyboardArrowRight, - null, - modifier = Modifier.size(16.dp), - tint = AppTheme.colors.primary600 - ) - } - } -} - -@Composable -private fun ScannedPrescriptionOverview( - navController: NavController, - listState: LazyListState, - prescription: PrescriptionData.Scanned, - onSwitchRedeemed: (redeemed: Boolean) -> Unit, - onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit -) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize(), - contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() - ) { - item { - Column( - Modifier - .fillMaxWidth() - .padding(PaddingDefaults.Medium), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - stringResource(R.string.pres_details_scanned_prescription) + " " + scannedPrescriptionIndex, - style = AppTheme.typography.h5, - textAlign = TextAlign.Center - ) - SpacerShortMedium() - Row(verticalAlignment = Alignment.CenterVertically) { - ScannedChip(onClick = { - onShowInfo(PrescriptionDetailBottomSheetContent.Scanned()) - }) - if (prescription.task.communications.isNotEmpty()) { - SpacerShortMedium() - SentStatusChip() - } - } - - SpacerShortMedium() - val date = dateWithIntroductionString(R.string.prs_low_detail_scanned_on, prescription.scannedOn) - Text(date, style = AppTheme.typography.body2l) - } - } - - item { - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = PaddingDefaults.Medium) - ) { - SpacerXLarge() - RedeemedButton( - modifier = Modifier.align(Alignment.CenterHorizontally), - redeemed = prescription.isRedeemed, - onSwitchRedeemed = onSwitchRedeemed - ) - SpacerXXLarge() - } - } - - item { - Label( - text = stringResource(R.string.pres_detail_technical_information), - onClick = { - navController.navigate(PrescriptionDetailsNavigationScreens.TechnicalInformation.path()) - } - ) - } - - item { - HealthPortalLink(Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.XXLarge)) - } - } -} - -@Composable -private fun RedeemedButton( - modifier: Modifier, - redeemed: Boolean, - onSwitchRedeemed: (redeemed: Boolean) -> Unit -) { - val buttonText = if (redeemed) { - stringResource(R.string.scanned_prescription_details_mark_as_unredeemed) - } else { - stringResource(R.string.scanned_prescription_details_mark_as_redeemed) - } - - PrimaryButtonSmall( - onClick = { - onSwitchRedeemed(!redeemed) - }, - modifier = modifier - ) { - Text(buttonText) - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt b/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt deleted file mode 100644 index 32fe12f4..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.prescription.detail.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData -import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.flow.Flow -import org.kodein.di.compose.rememberInstance - -@Stable -class PrescriptionDetailsController( - val prescriptionUseCase: PrescriptionUseCase -) : DeletePrescriptionsBridge { - suspend fun prescriptionDetailsFlow(taskId: String): Flow = - prescriptionUseCase.generatePrescriptionDetails(taskId) - - suspend fun redeemScannedTask(taskId: String, redeem: Boolean) { - prescriptionUseCase.redeemScannedTask(taskId, redeem) - } - - override suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result = - prescriptionUseCase.deletePrescription(profileId = profileId, taskId = taskId) -} - -@Composable -fun rememberPrescriptionDetailsController(): PrescriptionDetailsController { - val prescriptionUseCase by rememberInstance() - - return remember { - PrescriptionDetailsController(prescriptionUseCase) - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileController.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileController.kt deleted file mode 100644 index 98328398..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileController.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import android.graphics.Bitmap -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.ProfileAvatarUseCase -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 kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import org.kodein.di.compose.rememberInstance - -class ProfilesController( - private val profilesUseCase: ProfilesUseCase, - private val profilesWithPairedDevicesUseCase: ProfilesWithPairedDevicesUseCase, - private val profileAvatarUseCase: ProfileAvatarUseCase -) : ProfileBridge { - private val profilesFlow = profilesUseCase.profiles.map { ProfilesStateData.ProfilesState(it) } - - val profilesState - @Composable - get() = profilesFlow.collectAsState(ProfilesStateData.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) - - @Requirement( - "O.Tokn_6#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "invalidate config and token " - ) - 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) - } - } - - override suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) { - profilesUseCase.switchActiveProfile(profile) - } - - override val profiles: Flow> = - profilesUseCase.profiles - - override suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { - profilesUseCase.switchProfileToPKV(profileId) - } - - suspend fun updateProfileColor(profile: ProfilesUseCaseData.Profile, color: ProfilesData.ProfileColorNames) { - profilesUseCase.updateProfileColor(profile, color) - } - - suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: Bitmap) { - profileAvatarUseCase.savePersonalizedProfileImage(profileId, profileImage) - } - - suspend fun updateProfileName(profileId: ProfileIdentifier, newName: String) { - profilesUseCase.updateProfileName(profileId, newName) - } - - suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatarFigure: ProfilesData.AvatarFigure) { - profileAvatarUseCase.saveAvatarFigure(profileId, avatarFigure) - } - - suspend fun clearPersonalizedImage(profileId: ProfileIdentifier) { - profileAvatarUseCase.clearPersonalizedImage(profileId) - } -} - -@Composable -fun rememberProfilesController(): ProfilesController { - val profilesUseCase by rememberInstance() - val profilesWithPairedDevicesUseCase by rememberInstance() - val profileAvatarUseCase by rememberInstance() - return remember { - ProfilesController( - profilesUseCase, - profilesWithPairedDevicesUseCase, - profileAvatarUseCase - ) - } -} - -object ProfilesStateData { - @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() - ) - - val defaultProfile = ProfilesUseCaseData.Profile( - id = "", - name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( - insuranceType = ProfilesUseCaseData.InsuranceType.NONE - ), - active = false, - color = ProfilesData.ProfileColorNames.SPRING_GRAY, - lastAuthenticated = null, - ssoTokenScope = null, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage - ) -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt deleted file mode 100644 index b5cd4a42..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.ui - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.staticCompositionLocalOf -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn - -interface ProfileBridge { - val profiles: Flow> - suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) - suspend fun switchProfileToPKV(profileId: ProfileIdentifier) -} - -@Stable -class ProfileHandler( - private val bridge: ProfileBridge, - coroutineScope: CoroutineScope, - activeDefaultProfile: ProfilesUseCaseData.Profile = ProfilesStateData.defaultProfile -) { - enum class ProfileConnectionState { - LoggedIn, - LoggedOutWithoutTokenBiometrics, - LoggedOutWithoutToken, - LoggedOut, - NeverConnected - } - - private fun ProfilesUseCaseData.Profile.neverConnected() = ssoTokenScope == null && lastAuthenticated == null - - private fun ProfilesUseCaseData.Profile.ssoTokenSetAndConnected() = - ssoTokenScope?.token != null && ssoTokenScope.token?.isValid() == true - - private fun ProfilesUseCaseData.Profile.ssoTokenSetAndDisconnected() = - ssoTokenScope != null && ssoTokenScope.token?.isValid() == false || - lastAuthenticated != null - - private fun ProfilesUseCaseData.Profile.ssoTokenNotSet() = - when (ssoTokenScope) { - is IdpData.ExternalAuthenticationToken, - is IdpData.AlternateAuthenticationToken, - is IdpData.AlternateAuthenticationWithoutToken, - is IdpData.DefaultToken -> ssoTokenScope.token == null - - null -> true - } - - private fun ProfilesUseCaseData.Profile.ssoTokenWithoutScope() = - when (ssoTokenScope) { - is IdpData.AlternateAuthenticationWithoutToken -> true - else -> false - } - - @Stable - fun connectionState(profile: ProfilesUseCaseData.Profile): ProfileConnectionState? = - when { - profile.neverConnected() -> - ProfileConnectionState.NeverConnected - - profile.ssoTokenWithoutScope() -> - ProfileConnectionState.LoggedOutWithoutTokenBiometrics - - profile.ssoTokenNotSet() -> - ProfileConnectionState.LoggedOutWithoutToken - - profile.ssoTokenSetAndConnected() -> - ProfileConnectionState.LoggedIn - - profile.ssoTokenSetAndDisconnected() -> - ProfileConnectionState.LoggedOut - - else -> null - } - - var activeProfile by mutableStateOf(activeDefaultProfile) - private set - - private var profilesFlow = - bridge - .profiles - .onEach { - activeProfile = it.find { it.active } ?: ProfilesStateData.defaultProfile - } - .shareIn(coroutineScope, SharingStarted.Eagerly, 1) - - val profiles: State> - @Composable - get() = profilesFlow.collectAsState(emptyList()) - - suspend fun switchActiveProfile(profile: ProfilesUseCaseData.Profile) { - bridge.switchActiveProfile(profile) - } - - suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { - bridge.switchProfileToPKV(profileId) - } -} - -private fun profileHandlerSaver( - profilesController: ProfilesController, - scope: CoroutineScope -): Saver = Saver( - save = { state -> - state.activeProfile.id - }, - restore = { savedState -> - ProfileHandler(profilesController, scope, ProfilesStateData.defaultProfile.copy(id = savedState)) - } -) - -@Composable -fun rememberProfileHandler(): ProfileHandler { - val profilesController = rememberProfilesController() - val coroutineScope = rememberCoroutineScope() - return rememberSaveable( - saver = profileHandlerSaver( - profilesController, - coroutineScope - ) - ) { - ProfileHandler(profilesController, coroutineScope) - } -} - -val LocalProfileHandler = - staticCompositionLocalOf { error("No profile state provided!") } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfileAvatarUsecase.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfileAvatarUsecase.kt deleted file mode 100644 index e4954b47..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfileAvatarUsecase.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.usecase - -import android.graphics.Bitmap -import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository -import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream - -private const val BitmapQuality = 100 - -class ProfileAvatarUseCase( - private val profilesRepository: ProfilesRepository, - private val dispatcher: DispatchProvider -) { - suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatarFigure: ProfilesData.AvatarFigure) { - withContext(dispatcher.IO) { - profilesRepository.saveAvatarFigure(profileId, avatarFigure) - } - } - - suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: Bitmap) { - withContext(dispatcher.IO) { - val outputStream = ByteArrayOutputStream() - profileImage.compress(Bitmap.CompressFormat.PNG, BitmapQuality, outputStream) - val byteArray: ByteArray = outputStream.toByteArray() - profilesRepository.savePersonalizedProfileImage(profileId, byteArray) - } - } - - suspend fun clearPersonalizedImage(profileId: ProfileIdentifier) { - withContext(dispatcher.IO) { - profilesRepository.clearPersonalizedProfileImage(profileId) - } - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt b/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt deleted file mode 100644 index 6a00adaf..00000000 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.profiles.usecase.model - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant - -object ProfilesUseCaseData { - - enum class InsuranceType { - GKV, - PKV, - NONE - } - data class ProfileInsuranceInformation( - val insurantName: String = "", - val insuranceIdentifier: String = "", - val insuranceName: String = "", - val insuranceType: InsuranceType = InsuranceType.NONE - ) - - @Immutable - data class Profile( - val id: ProfileIdentifier, - val name: String, - val insuranceInformation: ProfileInsuranceInformation, - val active: Boolean, - val color: ProfilesData.ProfileColorNames, - val avatarFigure: ProfilesData.AvatarFigure, - val personalizedImage: ByteArray? = null, - val lastAuthenticated: Instant? = null, - val ssoTokenScope: IdpData.SingleSignOnTokenScope? - ) { - fun ssoTokenValid(now: Instant = Clock.System.now()) = ssoTokenScope?.token?.isValid(now) ?: false - fun hasNoImageSelected() = this.avatarFigure == ProfilesData.AvatarFigure.PersonalizedImage && - this.personalizedImage == null - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Profile - - if (id != other.id) return false - if (name != other.name) return false - if (insuranceInformation != other.insuranceInformation) return false - if (active != other.active) return false - if (color != other.color) return false - if (avatarFigure != other.avatarFigure) return false - if (personalizedImage != null) { - if (other.personalizedImage == null) return false - if (!personalizedImage.contentEquals(other.personalizedImage)) return false - } else if (other.personalizedImage != null) return false - if (lastAuthenticated != other.lastAuthenticated) return false - if (ssoTokenScope != other.ssoTokenScope) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + insuranceInformation.hashCode() - result = 31 * result + active.hashCode() - result = 31 * result + color.hashCode() - result = 31 * result + avatarFigure.hashCode() - result = 31 * result + (personalizedImage?.contentHashCode() ?: 0) - result = 31 * result + (lastAuthenticated?.hashCode() ?: 0) - result = 31 * result + (ssoTokenScope?.hashCode() ?: 0) - return result - } - } - - @Immutable - data class PairedDevice( - val name: String, - val alias: String, - val connectedOn: Instant - ) { - @Stable - fun isOurDevice(alias: String) = this.alias == alias - } - - @Immutable - data class PairedDevices( - val devices: List - ) -} diff --git a/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt b/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt deleted file mode 100644 index 9e3c5204..00000000 --- a/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - -package de.gematik.ti.erp.app.pharmacy.ui - -import de.gematik.ti.erp.app.CoroutineTestRule -import de.gematik.ti.erp.app.fhir.model.PharmacyContacts -import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Instant -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -@ExperimentalCoroutinesApi -class PharmacySearchViewModelTest { - @get:Rule - val coroutineRule = CoroutineTestRule() - - private lateinit var orderState: PharmacyOrderState - private lateinit var useCase: PharmacySearchUseCase - private lateinit var oftenUseCase: PharmacyOverviewUseCase - private lateinit var profileUseCase: ProfilesUseCase - - private val profile = ProfilesUseCaseData.Profile( - id = "", - name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( - insuranceType = ProfilesUseCaseData.InsuranceType.NONE - ), - active = true, - color = ProfilesData.ProfileColorNames.SPRING_GRAY, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage, - lastAuthenticated = null, - ssoTokenScope = null - ) - - private val prescriptions = listOf( - PharmacyUseCaseData.PrescriptionOrder( - taskId = "A", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false - ), - PharmacyUseCaseData.PrescriptionOrder( - taskId = "B", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false - ), - PharmacyUseCaseData.PrescriptionOrder( - taskId = "C", - accessCode = "1234", - title = "Test", - timestamp = Instant.fromEpochSeconds(0, 0), - substitutionsAllowed = false - ) - ) - - private val pharmacy = PharmacyUseCaseData.Pharmacy( - id = "", - name = "Test - Pharmacy", - address = null, - location = null, - distance = null, - contacts = PharmacyContacts( - phone = "", - mail = "", - url = "", - pickUpUrl = "", - deliveryUrl = "", - onlineServiceUrl = "" - ), - provides = listOf(), - openingHours = null, - telematikId = "" - ) - - private val orderOption = PharmacyScreenData.OrderOption.PickupService - - private val contacts = PharmacyUseCaseData.ShippingContact( - name = "Beate Muster", - line1 = "Friedrichstraße 136", - line2 = "", - postalCode = "10117", - city = "Berlin", - telephoneNumber = "", - mail = "", - deliveryInformation = "" - ) - - @Before - fun setUp() { - useCase = mockk() - profileUseCase = mockk() - oftenUseCase = mockk() - coEvery { useCase.prescriptionDetailsForOrdering("0") } returns flowOf( - PharmacyUseCaseData.OrderState( - prescriptions = prescriptions, - contact = contacts - ) - ) - orderState = PharmacyOrderState( - profile = ProfilesUseCaseData.Profile( - id = "0", - name = "Test", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), - active = false, - color = ProfilesData.ProfileColorNames.PINK, - lastAuthenticated = null, - ssoTokenScope = null, - personalizedImage = null, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage - ), - useCase = useCase, - scope = CoroutineScope(coroutineRule.dispatchers.Unconfined) - ) - coEvery { profileUseCase.profiles } returns flowOf(listOf(profile)) - orderState.onSelectPharmacy(pharmacy, orderOption) - } - - @Test - fun `order screen state - default`() = runTest { - val state = orderState.orderFlow.first() - - assertEquals(contacts, state.contact) - assertEquals(prescriptions, state.prescriptions) - } - - @Test - fun `order screen state - select prescriptions`() = runTest { - orderState.onSelectPrescription(prescriptions[0]) - orderState.onSelectPrescription(prescriptions[1]) - orderState.onSelectPrescription(prescriptions[2]) - - orderState.onDeselectPrescription(prescriptions[0]) - - val state = orderState.orderFlow.first() - - assertEquals(contacts, state.contact) - assertEquals( - listOf(prescriptions[1], prescriptions[2]), - state.prescriptions - ) - } - - @Test - fun `order screen state - set contacts`() = runTest { - coEvery { useCase.saveShippingContact(any()) } answers {} - coEvery { useCase.prescriptionDetailsForOrdering("") } returns flowOf( - PharmacyUseCaseData.OrderState( - prescriptions = prescriptions, - contact = contacts - ) - ) - - orderState.onSaveContact(contacts) - - coroutineRule.testDispatcher.scheduler.runCurrent() - coVerify(exactly = 1) { useCase.saveShippingContact(contacts) } - - val state = orderState.orderFlow.first() - - assertEquals(contacts, state.contact) - assertEquals(prescriptions, state.prescriptions) - } -} diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/android-mock/.gitignore b/app/android-mock/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/android-mock/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/android-mock/build.gradle.kts b/app/android-mock/build.gradle.kts new file mode 100644 index 00000000..973500a7 --- /dev/null +++ b/app/android-mock/build.gradle.kts @@ -0,0 +1,208 @@ +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject +import de.gematik.ti.erp.overriding +import org.owasp.dependencycheck.reporting.ReportGenerator.Format + +// TODO: Duplicate of android build.gradle, make this into one +plugins { + id("com.android.application") + kotlin("android") + id("org.jetbrains.compose") + id("io.realm.kotlin") + kotlin("plugin.serialization") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("de.gematik.ti.erp.dependencies") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") +} + +val VERSION_CODE: String by overriding() +val VERSION_NAME: String by overriding() +val TEST_INSTRUMENTATION_ORCHESTRATOR: String? by project + +afterEvaluate { + val taskRegEx = """assemble(Google|Huawei)(PuExternalDebug|PuExternalRelease)""".toRegex() + tasks.forEach { task -> + taskRegEx.matchEntire(task.name)?.let { + val (_, version, flavor) = it.groupValues + task.dependsOn(tasks.getByName("license${version}${flavor}Report")) + } + } +} + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +android { + namespace = "de.gematik.ti.erp.app.mock" + defaultConfig { + applicationId = "de.gematik.ti.erp.app.mock" + versionCode = VERSION_CODE.toInt() + versionName = VERSION_NAME + + testApplicationId = "de.gematik.ti.erp.app" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + androidResources { + noCompress("srt", "csv", "json") + } + kotlinOptions { + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + dependencyCheck { + analyzers.assemblyEnabled = false + suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" + formats = listOf(Format.HTML, Format.XML) + scanConfigurations = configurations.filter { + it.name.startsWith("api") || + it.name.startsWith("implementation") || + it.name.startsWith("kapt") + }.map { + it.name + } + } + + buildTypes { + val release by getting { + resValue("string", "app_label", "E-Rezept Mock") + } + val debug by getting { + applicationIdSuffix = ".debug" + resValue("string", "app_label", "E-Rezept Mock") + versionNameSuffix = "-debug" + } + } + + packagingOptions { + resources { + excludes += "META-INF/**" + // for JNA and JNA-platform + excludes += "META-INF/AL2.0" + excludes += "META-INF/LGPL2.1" + // for byte-buddy + excludes += "META-INF/licenses/ASM" + pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" + pickFirsts += "win32-x86/attach_hotspot_windows.dll" + } + } +} + +dependencies { + implementation(project(":app:features")) + androidTestImplementation(project(":app:shared-test")) + implementation(project(":common")) + testImplementation(project(":common")) + testImplementation(kotlin("test")) + implementation("com.tom-roush:pdfbox-android:2.0.27.0") { + exclude(group = "org.bouncycastle") + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + } + + inject { + dateTime { + implementation(datetime) + testCompileOnly(datetime) + } + android { + coreLibraryDesugaring(desugaring) + debugImplementation(processPhoenix) + } + androidX { + implementation(appcompat) + implementation(composeNavigation) + implementation(security) + implementation(lifecycleViewmodel) + implementation(lifecycleProcess) + implementation(lifecycleComposeRuntime) + } + dependencyInjection { + compileOnly(kodeinCompose) + androidTestImplementation(kodeinCompose) + } + logging { + implementation(napier) + } + tracking { + implementation(contentSquare) + } + compose { + implementation(runtime) + implementation(foundation) + implementation(uiTooling) + implementation(preview) + } + crypto { + testImplementation(jose4j) + testImplementation(bouncycastleBcprov) + testImplementation(bouncycastleBcpkix) + } + database { + compileOnly(realm) + testCompileOnly(realm) + } + network { + implementation(retrofit) + implementation(retrofit2KotlinXSerialization) + implementation(okhttp3) + implementation(okhttpLogging) + // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). + // Can be removed as soon as Retrofit releases a new version >2.9.0. + implementation(okio) + + androidTestImplementation(okhttp3) + } + playServices { + implementation(appUpdate) + } + serialization { + implementation(kotlinXJson) + } + androidXTest { + testImplementation(archCore) + androidTestImplementation(core) + androidTestImplementation(rules) + androidTestImplementation(junitExt) + androidTestImplementation(runner) + androidTestUtil(orchestrator) + androidTestUtil(services) + // androidTestImplementation(navigation) + androidTestImplementation(espresso) + androidTestImplementation(espressoIntents) + } + tracing { + debugImplementation(tracing) + implementation(tracing) + } + coroutinesTest { + testImplementation(coroutinesTest) + } + composeTest { + androidTestImplementation(ui) + debugImplementation(uiManifest) + androidTestImplementation(junit4) + } + networkTest { + testImplementation(mockWebServer) + } + test { + testImplementation(junit4) + testImplementation(snakeyaml) + testImplementation(json) + testImplementation(mockk) + androidTestImplementation(mockkAndroid) + } + } +} + +secrets { + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() + ) "ci-overrides.properties" else "gradle.properties" +} diff --git a/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt new file mode 100644 index 00000000..224a2801 --- /dev/null +++ b/app/android-mock/src/androidTest/kotlin/de/gematik/ti/erp/app/PharmacyUITest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import de.gematik.ti.erp.app.sharedtest.testresources.actions.PharmacyScreenAction + +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PharmacyUITest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val actions = PharmacyScreenAction(composeRule) + + @Test + fun pickupServiceSuccessTest() { + actions.pickupServiceSuccessTest() + } + + @Test + fun courierDeliverySuccessTest() { + actions.courierDeliverySuccessTest() + } + + @Test + fun pickupServiceMailDeliverySuccessTest() { + actions.pickupServiceMailDeliverySuccessTest() + } + + @Test + fun mailDeliverySuccessTest() { + actions.mailDeliverySuccessTest() + } + + @Test + fun pickupServiceCourierSuccessTest() { + actions.pickupServiceCourierSuccessTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { + actions.pickupServiceMailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun mailDeliveryCourierDeliverySuccessTest() { + actions.mailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun pickupServiceFailTest() { + actions.pickupServiceFailTest() + } + + @Test + fun courierDeliveryFailTest() { + actions.courierDeliveryFailTest() + } + + @Test + fun mailDeliveryFailTest() { + actions.mailDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliveryFailTest() { + actions.pickupServiceMailDeliveryCourierDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryFailTest() { + actions.pickupServiceMailDeliveryFailTest() + } + +} diff --git a/app/android-mock/src/main/AndroidManifest.xml b/app/android-mock/src/main/AndroidManifest.xml new file mode 100644 index 00000000..075a4453 --- /dev/null +++ b/app/android-mock/src/main/AndroidManifest.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt new file mode 100644 index 00000000..8a327f4f --- /dev/null +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/DefaultErezeptMockApp.kt @@ -0,0 +1,45 @@ +/* + * "${GEMATIK_COPYRIGHT_STATEMENT}" + */ + +package de.gematik.ti.erp.app + +import androidx.lifecycle.ProcessLifecycleOwner +import com.contentsquare.android.Contentsquare +import com.tom_roush.pdfbox.android.PDFBoxResourceLoader +import de.gematik.ti.erp.app.di.allModules +import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +class DefaultErezeptMockApp : ErezeptApp(), DIAware { + + override val di by DI.lazy { + import(androidXModule(this@DefaultErezeptMockApp)) + importAll(allModules) + bindSingleton { AuthenticationUseCase(instance()) } + bindSingleton { VisibleDebugTree() } + } + + private val authUseCase: AuthenticationUseCase by instance() + + private val visibleDebugTree: VisibleDebugTree by instance() + + override fun onCreate() { + super.onCreate() + if (BuildKonfig.INTERNAL) { + Napier.base(DebugAntilog()) + Napier.base(visibleDebugTree) + } + ProcessLifecycleOwner.get().lifecycle.apply { + addObserver(authUseCase) + } + PDFBoxResourceLoader.init(this) + Contentsquare.start(this) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt index 94bc9fb9..b5d322a4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/di/AllModules.kt @@ -1,21 +1,3 @@ -/* - * Copyright (c) 2024 gematik GmbH - * - * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by - * the European Commission - subsequent versions of the EUPL (the Licence); - * You may not use this work except in compliance with the Licence. - * You may obtain a copy of the Licence at: - * - * https://joinup.ec.europa.eu/software/page/eupl - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the Licence is distributed on an "AS IS" basis, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Licence for the specific language governing permissions and - * limitations under the Licence. - * - */ - package de.gematik.ti.erp.app.di import android.content.Context @@ -32,7 +14,7 @@ import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager import de.gematik.ti.erp.app.idp.idpModule import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule import de.gematik.ti.erp.app.orders.messagesModule -import de.gematik.ti.erp.app.pharmacy.pharmacyModule +import de.gematik.ti.erp.app.pharmacy.pharmacyMockModule import de.gematik.ti.erp.app.pkv.pkvModule import de.gematik.ti.erp.app.prescription.prescriptionModule import de.gematik.ti.erp.app.prescription.taskModule @@ -46,6 +28,24 @@ import org.kodein.di.bindProvider import org.kodein.di.bindSingleton import org.kodein.di.instance +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + private const val PREFERENCES_FILE_NAME = "appPrefs" private const val NETWORK_SECURE_PREFS_FILE_NAME = "networkingSecurePrefs" private const val NETWORK_PREFS_FILE_NAME = "networkingPrefs" @@ -99,7 +99,7 @@ val allModules = DI.Module("allModules") { idpModule, messagesModule, orderHealthCardModule, - pharmacyModule, + pharmacyMockModule, redeemModule, prescriptionModule, profilesModule, diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt similarity index 81% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt index b9262614..78db223f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyTestModule.kt @@ -15,29 +15,33 @@ * limitations under the Licence. * */ + package de.gematik.ti.erp.app.pharmacy -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyLocalDataSource import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyMockRepository import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRemoteDataSource +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase +import de.gematik.ti.erp.app.repository.PharmacyMockRepository import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance -val pharmacyTestModule = DI.Module("pharmacyTestModule") { +val pharmacyMockModule = DI.Module("pharmacyTestModule") { bindProvider { PharmacyRemoteDataSource(instance(), instance()) } - bindProvider { PharmacyLocalDataSource(instance()) } + bindProvider { DefaultPharmacyLocalDataSource(instance()) } bindProvider { ShippingContactRepository(instance(), instance()) } bindProvider { PharmacyDirectRedeemUseCase(instance()) } bindProvider { PharmacyMapsUseCase(instance(), instance(), instance()) } bindProvider { PharmacySearchUseCase(instance(), instance(), instance(), instance(), instance()) } bindProvider { PharmacyOverviewUseCase(instance(), instance()) } + bindProvider { GetOrderStateUseCase(instance(), instance(), instance()) } bindProvider { PharmacyMockRepository() } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyMockRepository.kt b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt similarity index 99% rename from common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyMockRepository.kt rename to app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt index c1375951..0788e7b0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyMockRepository.kt +++ b/app/android-mock/src/main/java/de/gematik/ti/erp/app/repository/PharmacyMockRepository.kt @@ -16,12 +16,13 @@ * */ -package de.gematik.ti.erp.app.pharmacy.repository +package de.gematik.ti.erp.app.repository import de.gematik.ti.erp.app.fhir.model.PharmacyServices import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices import de.gematik.ti.erp.app.fhir.model.json import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow diff --git a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..5d5c99d6 --- /dev/null +++ b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..5d5c99d6 --- /dev/null +++ b/app/android-mock/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/android-mock/src/main/res/raw/pharmacy_demo.json b/app/android-mock/src/main/res/raw/pharmacy_demo.json new file mode 100644 index 00000000..d735ef1f --- /dev/null +++ b/app/android-mock/src/main/res/raw/pharmacy_demo.json @@ -0,0 +1,1892 @@ +{ + "id":"49b6b9fd-eec7-41f3-b624-cc99d46fb828", + "meta":{ + "lastUpdated":"2023-08-31T12:18:10.94674676+02:00" + }, + "resourceType":"Bundle", + "type":"searchset", + "total":20, + "link":[ + { + "relation":"self", + "url":"Bundle49b6b9fd-eec7-41f3-b624-cc99d46fb828" + } + ], + "entry":[ + { + "resource":{ + "id":"6bb01538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 01" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.201" + } + ], + "name":"ZoTI_01_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb02538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 02" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.202" + } + ], + "name":"ZoTI_02_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb03538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 03" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.203" + } + ], + "name":"ZoTI_03_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb04538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 04" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.204" + } + ], + "name":"ZoTI_04_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":300 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb05538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 05" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.205" + } + ], + "name":"ZoTI_05_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb06538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"2d1f1f35-d03d-4932-a78a-67715cbb7963", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb06538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 06" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.206" + } + ], + "name":"ZoTI_06_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb07538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 07" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.207" + } + ], + "name":"ZoTI_07_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb08538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 08" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.208" + } + ], + "name":"ZoTI_08_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb09538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 09" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.209" + } + ], + "name":"ZoTI_09_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb10538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"fe9a01e8-d702-4b9d-a997-096eca057b74", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb10538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 10" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.210" + } + ], + "name":"ZoTI_10_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb11538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"0991992b-b3fd-4f3e-a331-d4f0e2856185", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb11538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 11" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.211" + } + ], + "name":"ZoTI_11_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb12538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"ae48f60e-9c17-4610-a0c4-d1f7ac6abb5b", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb12538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 12" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.212" + } + ], + "name":"ZoTI_12_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb13538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"e63f85da-3c1a-4f16-8059-45321bec107f", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb13538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 13" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.213" + } + ], + "name":"ZoTI_13_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb14538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"bb022669-f8fc-424a-8bfa-e9b5e8102333", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb14538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 14" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.214" + } + ], + "name":"ZoTI_14_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb15538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 15" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.215" + } + ], + "name":"ZoTI_15_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb16538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 16" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.216" + } + ], + "name":"ZoTI_16_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb17538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"0f4ae22e-f717-47b1-893e-1d41684c8579", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb17538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 17" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.217" + } + ], + "name":"ZoTI_17_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb18538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 18" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.218" + } + ], + "name":"ZoTI_18_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/pick_up/", + "use":"mobile", + "rank":100 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb19538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"2" + }, + "resourceType":"Location", + "contained":[ + { + "id":"72ab1d02-d3e2-4af1-891f-a476c23eaf44", + "resourceType":"HealthcareService", + "active":true, + "coverageArea":[ + { + "extension":[ + { + "url":"https://ngda.de/fhir/extensions/ServiceCoverageRange", + "valueQuantity":{ + "value":10000, + "unit":"m" + } + } + ] + } + ], + "location":[ + { + "reference":"/Location/6bb1 2023-08-31 12:18:11.626 28938-31101 OkHttp de.gematik.ti.erp.app.test D 9538-5924-4be3-98ff-7475d27aee4f" + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"498", + "display":"Mobile Services" + } + ] + } + ] + } + ], + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 19" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.219" + } + ], + "name":"ZoTI_19_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "telecom":[ + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/delivery_only", + "use":"mobile", + "rank":300 + }, + { + "system":"other", + "value":"https://erp-pharmacy-serviceprovider.dev.gematik.solutions/local_delivery/?req=", + "use":"mobile", + "rank":200 + } + ], + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"PHARM", + "display":"pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + }, + { + "resource":{ + "id":"6bb20538-5924-4be3-98ff-7475d27aee4f", + "meta":{ + "lastUpdated":"2023-08-07T10:48:51.845+02:00", + "versionId":"3" + }, + "resourceType":"Location", + "address":{ + "type":"physical", + "line":[ + "ZoTIstr. 20" + ], + "postalCode":"10117", + "city":"ZoTI-Town", + "country":"D" + }, + "hoursOfOperation":[ + { + "daysOfWeek":[ + "mon", + "tue", + "wed", + "thu", + "fri" + ], + "openingTime":"08:00:00", + "closingTime":"18:00:00" + } + ], + "identifier":[ + { + "system":"https://gematik.de/fhir/NamingSystem/TelematikID", + "value":"3-01.2.2023001.16.220" + } + ], + "name":"ZoTI_20_TEST-ONLY", + "position":{ + "latitude":13.387627883956709, + "longitude":52.5226398750957 + }, + "status":"active", + "type":[ + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/service-type", + "code":"DELEGATOR", + "display":"eRX Token Receiver" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"OUTPHARM", + "display":"outpatient pharmacy" + } + ] + }, + { + "coding":[ + { + "system":"http://terminology.hl7.org/CodeSystem/v3-RoleCode", + "code":"MOBL", + "display":"Mobile Services" + } + ] + } + ] + }, + "search":{ + "mode":"match" + } + } + ] +} \ No newline at end of file diff --git a/app/android-mock/src/main/res/values/strings.xml b/app/android-mock/src/main/res/values/strings.xml new file mode 100644 index 00000000..871839b8 --- /dev/null +++ b/app/android-mock/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + E-Rezept Mock + \ No newline at end of file diff --git a/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt b/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt new file mode 100644 index 00000000..4fe4c56e --- /dev/null +++ b/app/android-mock/src/test/java/de/gematik/ti/erp/app/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package de.gematik.ti.erp.app + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/android/build.gradle.kts b/app/android/build.gradle.kts similarity index 65% rename from android/build.gradle.kts rename to app/android/build.gradle.kts index 6c29309b..dedd7fa9 100644 --- a/android/build.gradle.kts +++ b/app/android/build.gradle.kts @@ -1,4 +1,7 @@ -import de.gematik.ti.erp.app + +import de.gematik.ti.erp.AppDependenciesPlugin +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject import de.gematik.ti.erp.overriding import org.owasp.dependencycheck.reporting.ReportGenerator.Format import java.util.Properties @@ -7,15 +10,13 @@ plugins { id("com.android.application") kotlin("android") id("org.jetbrains.compose") - kotlin("plugin.serialization") id("io.realm.kotlin") - id("kotlin-parcelize") + kotlin("plugin.serialization") id("org.owasp.dependencycheck") id("com.jaredsburrows.license") id("de.gematik.ti.erp.dependencies") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") - id("shot") } val VERSION_CODE: String by overriding() @@ -44,37 +45,19 @@ licenseReport { } android { - namespace = "de.gematik.ti.erp.app" + namespace = AppDependenciesPlugin.APP_NAME_SPACE defaultConfig { - applicationId = "de.gematik.ti.erp.app" + applicationId = AppDependenciesPlugin.APP_ID versionCode = VERSION_CODE.toInt() versionName = VERSION_NAME testApplicationId = "de.gematik.ti.erp.app.test.test" - testInstrumentationRunner = "com.karumi.shot.ShotTestRunner" - testInstrumentationRunnerArguments += "clearPackageData" to "true" - testInstrumentationRunnerArguments += "useTestStorageService" to "true" - } - - androidResources { - noCompress("srt", "csv", "json") - } - - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - kotlinOptions { - jvmTarget = "17" + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } - - testOptions { - execution = "ANDROIDX_TEST_ORCHESTRATOR" - } - dependencyCheck { analyzers.assemblyEnabled = false suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" @@ -87,8 +70,8 @@ android { it.name } } - - val signingPropsFile = project.rootProject.file("signing.properties") + val rootPath = project.rootProject + val signingPropsFile = rootPath.file("signing.properties") if (signingPropsFile.canRead()) { println("Signing properties found: $signingPropsFile") val signingProps = Properties() @@ -97,7 +80,9 @@ android { fun creatingRelease() = creating { val target = this.name // property name; e.g. googleRelease println("Create signing config for: $target") - storeFile = signingProps["$target.storePath"]?.let { file(it) } + storeFile = signingProps["$target.storePath"]?.let { + rootPath.file("erp-app-android/$it") + } println("\tstore: ${signingProps["$target.storePath"]}") keyAlias = signingProps["$target.keyAlias"] as? String println("\tkeyAlias: ${signingProps["$target.keyAlias"]}") @@ -119,7 +104,10 @@ android { val release by getting { isMinifyEnabled = true isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) if (signingPropsFile.canRead()) { signingConfig = signingConfigs.getByName("googleRelease") } @@ -127,11 +115,11 @@ android { } val debug by getting { applicationIdSuffix = ".test" - resValue("string", "app_label", "eRp-Test") + resValue("string", "app_label", "E-Rezept Debug") versionNameSuffix = "-debug" signingConfigs { getByName("debug") { - storeFile = file("$rootDir/keystore/debug.keystore") + storeFile = rootPath.file("keystore/debug.keystore") keyAlias = "androiddebugkey" storePassword = "android" keyPassword = "android" @@ -188,136 +176,82 @@ android { } } -compose.android.useAndroidX = true -compose.android.androidxVersion = app.composeVersion - -shot { - tolerance = 0.15 // Tests pass if less than 0,15% of the pixels differ - applicationId = "de.gematik.ti.erp.app" - runInstrumentation = true -} - dependencies { + implementation(project(":app:features")) + implementation(project(":app:demo-mode")) + androidTestImplementation(project(":app:shared-test")) implementation(project(":common")) testImplementation(project(":common")) - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) testImplementation(kotlin("test")) implementation("com.tom-roush:pdfbox-android:2.0.27.0") { exclude(group = "org.bouncycastle") + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) } - app { - dataMatrix { - implementation(mlkitBarcodeScanner) - implementation(zxing) - } - kotlinX { - implementation(coroutines("core")) - implementation(coroutines("android")) - implementation(coroutines("play-services")) + inject { + dateTime { implementation(datetime) testCompileOnly(datetime) } android { coreLibraryDesugaring(desugaring) - - implementation(legacySupport) + debugImplementation(processPhoenix) + } + androidX { implementation(appcompat) - implementation(coreKtx) - implementation(datastorePreferences) - implementation(security) - implementation(biometric) - implementation(webkit) - - implementation(mapsAndroidUtils) - implementation(maps) - implementation(mapsCompose) - implementation(mapsUtils) - - implementation(lifecycle("viewmodel-compose")) - implementation(lifecycle("process")) { - // FIXME: remove if AGP > 7.2.0-alpha05 can handle cyclic dependencies (again) - exclude(group = "androidx.lifecycle", module = "lifecycle-runtime") - } - implementation(composeNavigation) - implementation(composeActivity) - implementation(composePaging) - - implementation(camera("camera2")) - implementation(camera("lifecycle")) - implementation(camera("view", cameraViewVersion)) - implementation(imageCropper) - - debugImplementation(processPhoenix) + implementation(security) + implementation(lifecycleViewmodel) + implementation(lifecycleProcess) + implementation(lifecycleComposeRuntime) } dependencyInjection { - compileOnly(kodein("di-framework-compose")) - androidTestImplementation(kodein("di-framework-compose")) + compileOnly(kodeinCompose) + androidTestImplementation(kodeinCompose) } logging { implementation(napier) } - lottie { - implementation(lottie) + tracking { + implementation(contentSquare) } - serialization { - implementation(kotlinXJson) + compose { + implementation(runtime) + implementation(foundation) + implementation(uiTooling) + implementation(preview) } crypto { - implementation(jose4j) - implementation(bouncyCastle("bcprov")) - implementation(bouncyCastle("bcpkix")) - testImplementation(bouncyCastle("bcprov")) - testImplementation(bouncyCastle("bcpkix")) + testImplementation(jose4j) + testImplementation(bouncycastleBcprov) + testImplementation(bouncycastleBcpkix) + } + database { + testCompileOnly(realm) } network { - implementation(retrofit2("retrofit")) + implementation(retrofit) implementation(retrofit2KotlinXSerialization) - implementation(okhttp3("okhttp")) - implementation(okhttp3("logging-interceptor")) + implementation(okhttp3) + implementation(okhttpLogging) // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). // Can be removed as soon as Retrofit releases a new version >2.9.0. implementation(okio) - androidTestImplementation(okhttp3("okhttp")) + androidTestImplementation(okhttp3) } database { compileOnly(realm) testCompileOnly(realm) } - compose { - implementation(runtime) - implementation(foundation) - implementation(material) - implementation(materialIconsExtended) - implementation(animation) - implementation(uiTooling) - implementation(preview) - implementation(accompanist("swiperefresh")) - implementation(accompanist("flowlayout")) - implementation(accompanist("pager")) - implementation(accompanist("pager-indicators")) - implementation(accompanist("systemuicontroller")) - } - passwordStrength { - implementation(zxcvbn) - } - - contentSquare { - implementation(cts) - } - playServices { - implementation(location) - implementation(integrity) - implementation(appReview) implementation(appUpdate) - implementation(maps) } - - androidTest { + serialization { + implementation(kotlinXJson) + } + androidXTest { testImplementation(archCore) androidTestImplementation(core) androidTestImplementation(rules) @@ -325,11 +259,15 @@ dependencies { androidTestImplementation(runner) androidTestUtil(orchestrator) androidTestUtil(services) - androidTestImplementation(navigation) + // androidTestImplementation(navigation) androidTestImplementation(espresso) androidTestImplementation(espressoIntents) } - kotlinXTest { + tracing { + debugImplementation(tracing) + implementation(tracing) + } + coroutinesTest { testImplementation(coroutinesTest) } composeTest { @@ -344,12 +282,13 @@ dependencies { testImplementation(junit4) testImplementation(snakeyaml) testImplementation(json) - testImplementation(mockk("mockk")) - androidTestImplementation(mockk("mockk-android")) + testImplementation(mockkOld) + androidTestImplementation(mockkAndroid) } } } secrets { - defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists()) "ci-overrides.properties" else "gradle.properties" + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() + ) "ci-overrides.properties" else "gradle.properties" } diff --git a/android/color_convert.ws.kts b/app/android/color_convert.ws.kts similarity index 100% rename from android/color_convert.ws.kts rename to app/android/color_convert.ws.kts diff --git a/android/proguard-rules.pro b/app/android/proguard-rules.pro similarity index 65% rename from android/proguard-rules.pro rename to app/android/proguard-rules.pro index 582c87a5..d10b8ab7 100644 --- a/android/proguard-rules.pro +++ b/app/android/proguard-rules.pro @@ -46,13 +46,27 @@ -keep class androidx.fragment.app.FragmentContainerView -keep class de.gematik.ti.erp.app.common.usecase.model.** { *; } +# Demo mode +-keep class de.gematik.ti.erp.app.demomode.** { *; } + +# ContentSquare +-keep class com.contentsquare.android.sdk.** { *; } + +# MLKit +# -keep class com.google.android.gms.** { *; } + # Realm +-keep class de.gematik.ti.erp.app.db.** { *; } -keep class de.gematik.ti.erp.app.db.entities.** { *; } -keep class de.gematik.ti.erp.app.db.LatestManualMigration { *; } -keep class de.gematik.ti.erp.app.db.LatestManualMigration$Companion { *; } # companion is autogenerated -keep class io.realm.** { *; } --keep class kotlin.jvm.** { *; } +-keep class de.gematik.ti.erp.app.di.** { *; } +-keep class de.gematik.ti.erp.app.MessageConversionException +# Serilization +-keep class kotlin.jvm.** { *; } +-keep class kotlin.reflect.** { *; } -keep class kotlinx.serialization.json.** { *; } # Keep `Companion` object fields of serializable classes. @@ -88,4 +102,37 @@ -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference -keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest -# -printusage r8/usages.txt \ No newline at end of file +-printusage r8/usages.txt + +# PDFbox +-dontwarn com.gemalto.jp2.JP2Decoder +-dontwarn com.gemalto.jp2.JP2Encoder +-dontwarn org.slf4j.impl.StaticLoggerBinder + +-keepclassmembers class kotlin.Metadata { + public ; +} +-keepclasseswithmembers class kotlin.reflect.jvm.internal.** { *; } +-keepclasseswithmembers class java.lang.reflect.** { *; } +-keepclasseswithmembers class java.lang.Class + +# https://github.com/square/retrofit/issues/3751 +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response +-keep,allowobfuscation,allowshrinking class okhttp3.RequestBody +-keep,allowobfuscation,allowshrinking class okhttp3.ResponseBody + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# https://github.com/square/okhttp/blob/339732e3a1b78be5d792860109047f68a011b5eb/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro#L11-L14 +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +-printconfiguration "~/tmp/full-r8-config.txt" + diff --git a/app/android/src/androidTest/AndroidManifest.xml b/app/android/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..4e10e648 --- /dev/null +++ b/app/android/src/androidTest/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt new file mode 100644 index 00000000..9b40c15b --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/TestConfig.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("ktlint:max-line-length") + +package de.gematik.ti.erp.app.test.test + +object TestConfig { + const val WeakPassword = "TrustNo1" + const val StrongPassword = "Jaja Ding Dong!" + const val DefaultProfileName = "Rainer Reizdarm" + const val DefaultEGKCAN = "123123" + const val DefaultEGKPassword = "123456" + const val WaitTimeout1Sec = 1_000L + const val ScreenChangeTimeout = 3_000L + const val LoadPrescriptionsTimeout = 20_000L + + const val AppDefaultVirtualEgkKvnr = "X764228532" + const val PharmacyName = "Apotheke am Flughafen - E2E-Test" + const val PharmacyTelematikId = "3-SMC-B-Testkarte-883110000116873" + + const val PharmacyZoti01 = "ZoTI_01_TEST-ONLY" + const val PharmacyZoti02 = "ZoTI_02_TEST-ONLY" + const val PharmacyZoti03 = "ZoTI_03_TEST-ONLY" + const val PharmacyZoti04 = "ZoTI_04_TEST-ONLY" + const val PharmacyZoti05 = "ZoTI_05_TEST-ONLY" + const val PharmacyZoti06 = "ZoTI_06_TEST-ONLY" + const val PharmacyZoti07 = "ZoTI_07_TEST-ONLY" + const val PharmacyZoti08 = "ZoTI_08_TEST-ONLY" + const val PharmacyZoti09 = "ZoTI_09_TEST-ONLY" + const val PharmacyZoti10 = "ZoTI_10_TEST-ONLY" + const val PharmacyZoti11 = "ZoTI_11_TEST-ONLY" + const val PharmacyZoti12 = "ZoTI_12_TEST-ONLY" + const val PharmacyZoti13 = "ZoTI_13_TEST-ONLY" + const val PharmacyZoti14 = "ZoTI_14_TEST-ONLY" + const val PharmacyZoti15 = "ZoTI_15_TEST-ONLY" + const val PharmacyZoti16 = "ZoTI_16_TEST-ONLY" + const val PharmacyZoti17 = "ZoTI_17_TEST-ONLY" + const val PharmacyZoti18 = "ZoTI_18_TEST-ONLY" + const val PharmacyZoti19 = "ZoTI_19_TEST-ONLY" + const val PharmacyZoti20 = "ZoTI_20_TEST-ONLY" + + object FD { + const val DefaultServer = "https://erpps-test.dev.gematik.solutions" + const val DefaultDoctor = "9a15b6f9f4b8f2e9df1db745a4091bbd" + const val DefaultPharmacy = "886c6eda7dd5a1c6b1d112907f544d3" + } +} + +interface VirtualEgk { + val certificate: String + val privateKey: String + val kvnr: String + val name: String +} + +object VirtualEgk1 : VirtualEgk { + @Suppress("MaxLineLength") + override val certificate = + "MIIDXTCCAwSgAwIBAgIHAs9vZEwB8jAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAxMjQwMDAwMDBaFw0yNzAxMjMyMzU5NTlaMIHgMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDU5Mjk3MTEUMBIGA1UEBAwLVsOzcm13aW5rZWwxHDAaBgNVBCoME1hlbmlhIFZlcmEgQWRlbGhlaWQxEjAQBgNVBAwMCVByb2YuIERyLjE1MDMGA1UEAwwsUHJvZi4gRHIuIFhlbmlhIFZlcmEgQS4gVsOzcm13aW5rZWxURVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAEczsMfajcnKpGYyNeXUhODjyrX4z4j9Qzio/Ulq5COPVySk0CxYBDj+1VEd5FalhEJXC9HjVRCflQx+RkEQFbvqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBTCDfBZ8X30CZnFk7E2x8+lMM5uODA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIDDAXcyOKDYOZpoH0iYijr1yisyxHeT3ch6XZlFNXPrKAiAHepW4TOQAoqyoGG9Pgly0TO2tTB7WLKEc7B3F6lNhpA==" + override val privateKey = "AJzshqeIuhwReqZpWbqY0PnRjTdTRzk4Zj9GpSxcUukA" + override val kvnr = "X110592971" + override val name = "Vórmwinkel Xenia Vera Adelheid" +} + +object VirtualEgkWithPrescription : VirtualEgk { + @Suppress("MaxLineLength") + override val certificate = + "MIIDLTCCAtSgAwIBAgIHAZ/zfVKUfTAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAyMDQwMDAwMDBaFw0yNzAyMDMyMzU5NTlaMIGwMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDUzNTU0MTEOMAwGA1UEBAwFS2zDtm4xDTALBgNVBCoMBEx1Y2ExDDAKBgNVBAwMA0RyLjEgMB4GA1UEAwwXRHIuIEx1Y2EgS2zDtm5URVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAETn/MKYxsnBH9khicaXG3mFc5v4RoL0ILuJ3TreTsiFsv91OA6Yj/O4EAxm6dCpPtGgWRyVUYbOgDkaDurSUPpqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBRhIkfxtBhE+Z3fcu+OWu/3gnnYqjA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIGHDnSVg2A9NmFPhtzo4dL3CVbN94k3NrYhXLOZoCUFXAiBlE6TfW6uL91jhv8JuupHhr7X6B9YcbVizWoMxo1grFA==" + override val privateKey = "cv2z1KGMJi+M5foz3GCz0bi5pSdBIjVTqw2cUuIsJcY=" + override val kvnr = "X110535541" + override val name = "Klön Luca" +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt new file mode 100644 index 00000000..2f5e22d9 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/WithFontScale.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test + +import de.gematik.ti.erp.app.test.test.core.execShellCmd +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +open class WithFontScale(protected val fontScale: String) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: fontScale={0}") + fun data(): Collection> { + return listOf( + arrayOf("1.0"), + arrayOf("1.3") + ) + } + + @JvmStatic + @BeforeClass + fun disableGestureNavbar() { + execShellCmd("cmd overlay disable com.android.internal.systemui.navbar.gestural") + } + + @JvmStatic + @AfterClass + fun enableGestureNavbar() { + execShellCmd("cmd overlay enable com.android.internal.systemui.navbar.gestural") + } + } + + @Before + fun changeScales() { + execShellCmd("settings put system font_scale $fontScale") + } + + @After + fun resetScales() { + execShellCmd("settings put system font_scale 1.0") + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt new file mode 100644 index 00000000..1c9aca52 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/TaskCollection.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core + +import android.util.Log +import de.gematik.ti.erp.app.TestWrapper +import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox +import de.gematik.ti.erp.app.test.test.core.prescription.Prescription +import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionDoctorUseCase +import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionPharmacyUseCase +import de.gematik.ti.erp.app.test.test.core.prescription.Task +import de.gematik.ti.erp.app.test.test.core.prescription.retry +import org.junit.Assume +import org.junit.AssumptionViolatedException + +private val doctorUseCase by lazy { PrescriptionDoctorUseCase() } +private val pharmacyUseCase by lazy { PrescriptionPharmacyUseCase() } + +class TaskCollection( + data: List, + private val testWrapper: TestWrapper +) { + data class TaskAndPrescription( + val task: Task, + val prescription: Prescription, + val isDispensed: Boolean = false + ) + + private val _taskData = mutableListOf() + .apply { + addAll(data) + } + + val taskData: List + get() = _taskData + + fun deleteAll() { + // clean up - allowed to throw, so test won't fail + taskData.forEach { (task, _, isDispensed) -> + if (task.secret == null || isDispensed) { + testWrapper.deleteTask(task.taskId) + } else { + runCatching { + pharmacyUseCase + .abortTask(taskId = task.taskId, accessCode = task.accessCode, secret = task.secret) + } + } + .onFailure { Log.e("deleteTask", "Deleting task with id `${task.taskId}` failed", it) } + .onSuccess { Log.d("deleteTask", "Task with id `${task.taskId}` deleted") } + } + } + + fun accept(data: TaskAndPrescription): Task { + val taskWithSecret = pharmacyUseCase.acceptTask(taskId = data.task.taskId, accessCode = data.task.accessCode) + requireNotNull(taskWithSecret.secret) { "Accepting a task must return secret!" } + + _taskData.replaceWith(data.copy(task = taskWithSecret)) + + return taskWithSecret + } + + fun reply(data: TaskAndPrescription, message: CommunicationPayloadInbox) { + pharmacyUseCase.replyWithCommunication( + taskId = data.task.taskId, + kvNr = data.prescription.patient!!.kvnr!!, + message = message + ) + } + + fun dispense(data: TaskAndPrescription) { + requireNotNull(data.task.secret) { "Task can be only dispensed with the `secret`" } + pharmacyUseCase.dispenseMedication( + taskId = data.task.taskId, + accessCode = data.task.accessCode, + secret = data.task.secret + ) + _taskData.replaceWith(data.copy(isDispensed = true)) + } + + companion object { + fun generate(count: Int, kvnr: String, testWrapper: TestWrapper): TaskCollection { + require(count >= 1) + + val taskData = (1..count).mapNotNull { + retry(3) { doctorUseCase.prescribeToPatient(kvnr) } + } + val prescriptions = taskData.mapNotNull { data -> + retry(3) { doctorUseCase.prescriptionDetails(data.taskId) } + } + + try { + Assume.assumeTrue( + "Prescription server couldn't create all tasks", + taskData.size == prescriptions.size && taskData.size == count + ) + } catch (e: AssumptionViolatedException) { + taskData.forEach { data -> + val taskId = data.taskId + testWrapper.deleteTask(taskId) + .onFailure { Log.e("deleteTask", "Deleting task with id `$taskId` failed", it) } + .onSuccess { Log.d("deleteTask", "Task with id `$taskId` deleted") } + } + throw e + } + + val data = taskData.zip(prescriptions) { tD: Task, p: Prescription -> + TaskAndPrescription( + task = tD, + prescription = p + ) + } + + Log.d("generate", "Prescriptions:\n${prescriptions.joinToString("\n\n") { it.toString() }}") + + return TaskCollection( + data, + testWrapper + ) + } + } +} + +private fun MutableList.replaceWith(data: TaskCollection.TaskAndPrescription) { + val ix = indexOfFirst { it.task.taskId == data.task.taskId } + require(ix != -1) + this[ix] = data +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt new file mode 100644 index 00000000..630f2f6e --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/Util.kt @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.printToString +import androidx.test.platform.app.InstrumentationRegistry +import de.gematik.ti.erp.app.InsuranceState +import de.gematik.ti.erp.app.MedicationCategory +import de.gematik.ti.erp.app.PharmacyId +import de.gematik.ti.erp.app.PrescriptionId +import de.gematik.ti.erp.app.SubstitutionAllowed +import de.gematik.ti.erp.app.SupplyForm + +fun ComposeTestRule.awaitDisplay(timeout: Long, vararg tags: String): String { + val t0 = System.currentTimeMillis() + do { + tags.forEach { tag -> + try { + onNodeWithTag(tag).assertIsDisplayed() + return tag + } catch (_: AssertionError) { + } + } + mainClock.advanceTimeBy(100) + Thread.sleep(100) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.awaitDisplay(timeout: Long, node: () -> SemanticsNodeInteraction) { + val t0 = System.currentTimeMillis() + do { + try { + node().assertIsDisplayed() + return + } catch (_: AssertionError) { + } + mainClock.advanceTimeBy(100) + Thread.sleep(100) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.await(timeout: Long, node: () -> Unit) { + val t0 = System.currentTimeMillis() + do { + try { + node() + return + } catch (_: AssertionError) { + } + mainClock.advanceTimeBy(100) + Thread.sleep(100) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.sleep(timeout: Long) { + val t0 = System.currentTimeMillis() + do { + mainClock.advanceTimeBy(100) + Thread.sleep(100) + } while (System.currentTimeMillis() - t0 < timeout) +} + +fun SemanticsNodeInteraction.assertHasText(includeEditableText: Boolean = true) = + assert(hasText(includeEditableText)) + +fun hasText( + includeEditableText: Boolean = true +): SemanticsMatcher { + val propertyName = if (includeEditableText) { + "${SemanticsProperties.Text.name} + ${SemanticsProperties.EditableText.name}" + } else { + SemanticsProperties.Text.name + } + return SemanticsMatcher( + propertyName + ) { node -> + val actual = mutableListOf() + if (includeEditableText) { + node.config.getOrNull(SemanticsProperties.EditableText) + ?.let { actual.add(it.text) } + } + node.config.getOrNull(SemanticsProperties.Text) + ?.let { actual.addAll(it.map { anStr -> anStr.text }) } + actual.all { it.isNotBlank() } + } +} + +fun SemanticsNodeInteractionCollection.assertNone( + matcher: SemanticsMatcher +): SemanticsNodeInteractionCollection = + filter(matcher) + .assertCountEquals(0) + +fun hasPrescriptionId(id: String): SemanticsMatcher = + SemanticsMatcher.expectValue(PrescriptionId, id) + +fun hasPharmacyId(id: String): SemanticsMatcher = + SemanticsMatcher.expectValue(PharmacyId, id) + +fun hasInsuranceState(state: String?): SemanticsMatcher = + SemanticsMatcher.expectValue(InsuranceState, state) + +fun hasSubstitutionAllowed(allowed: Boolean): SemanticsMatcher = + SemanticsMatcher.expectValue(SubstitutionAllowed, allowed) + +fun hasSupplyForm(form: String): SemanticsMatcher = + SemanticsMatcher.expectValue(SupplyForm, form) + +fun hasMedicationCategory(form: String): SemanticsMatcher = + SemanticsMatcher.expectValue(MedicationCategory, form) + +fun execShellCmd(cmd: String) { + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand(cmd) +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt new file mode 100644 index 00000000..5dacac56 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/CommunicationPayload.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class SupplyOptionsType { + @SerialName("shipment") + Shipment, + + @SerialName("onPremise") + OnPremise, + + @SerialName("delivery") + Delivery +} + +@Serializable +data class CommunicationPayloadInbox( + val version: String = "1", + val supplyOptionsType: SupplyOptionsType, + @SerialName("info_text") val infoText: String? = null, + val url: String? = null, + val pickUpCodeHR: String? = null, + val pickUpCodeDMC: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt new file mode 100644 index 00000000..4bbf6ac2 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Coverage.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Coverage( + @SerialName("iknr") + val iknr: String? = null, + @SerialName("insuranceKind") + val insuranceKind: String? = null, + @SerialName("insuranceName") + val insuranceName: String? = null, + @SerialName("insuranceState") + val insuranceState: String? = null, + @SerialName("payorType") + val payorType: String? = null, + @SerialName("personGroup") + val personGroup: String? = null, + @SerialName("wop") + val wop: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt new file mode 100644 index 00000000..66090324 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Medication.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Medication( + @SerialName("amount") + val amount: Int? = null, + @SerialName("category") + val category: String? = null, + @SerialName("dosage") + val dosage: String? = null, + @SerialName("freeText") + val freeText: String? = null, + @SerialName("ingredient") + val ingredient: String? = null, + @SerialName("ingredientStrength") + val ingredientStrength: String? = null, + @SerialName("name") + val name: String? = null, + @SerialName("packageQuantity") + val packageQuantity: Int? = null, + @SerialName("pzn") + val pzn: String? = null, + @SerialName("standardSize") + val standardSize: String? = null, + @SerialName("substitutionAllowed") + val substitutionAllowed: Boolean? = null, + @SerialName("supplyForm") + val supplyForm: String? = null, + @SerialName("type") + val type: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt new file mode 100644 index 00000000..554cd04f --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Patient.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Patient( + @SerialName("birthDate") + val birthDate: String? = null, + @SerialName("city") + val city: String? = null, + @SerialName("firstName") + val firstName: String? = null, + @SerialName("kvnr") + val kvnr: String? = null, + @SerialName("lastName") + val lastName: String? = null, + @SerialName("postal") + val postal: String? = null, + @SerialName("street") + val street: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt new file mode 100644 index 00000000..37fdd884 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Practitioner.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Practitioner( + @SerialName("bsnr") + val bsnr: String? = null, + @SerialName("city") + val city: String? = null, + @SerialName("email") + val email: String? = null, + @SerialName("hba") + val hba: String? = null, + @SerialName("id") + val id: String? = null, + @SerialName("lanr") + val lanr: String? = null, + @SerialName("name") + val name: String? = null, + @SerialName("officeName") + val officeName: String? = null, + @SerialName("phone") + val phone: String? = null, + @SerialName("postal") + val postal: String? = null, + @SerialName("smcb") + val smcb: String? = null, + @SerialName("street") + val street: String? = null, + @SerialName("ti") + val telematik: Telematik? = null, + @SerialName("type") + val type: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt new file mode 100644 index 00000000..3235cbaf --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Prescription.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Prescription( + @SerialName("accessCode") + val accessCode: String? = null, + @SerialName("authoredOn") + val authoredOn: Long? = null, + @SerialName("authoredOnFormatted") + val authoredOnFormatted: String? = null, + @SerialName("coverage") + val coverage: Coverage? = null, + @SerialName("medication") + val medication: Medication? = null, + @SerialName("patient") + val patient: Patient? = null, + @SerialName("practitioner") + val practitioner: Practitioner? = null, + @SerialName("prescriptionId") + val prescriptionId: String? = null, + @SerialName("taskId") + val taskId: String = "" +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt new file mode 100644 index 00000000..d482fa98 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionDoctorUseCase.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import android.util.Log +import de.gematik.ti.erp.app.test.test.TestConfig +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +private fun defaultPayload(kvnr: String) = """ +{ + "patient": { + "kvnr": "$kvnr" + }, + "medication": { + "category": "00" + } +} +""" + +class PrescriptionDoctorUseCase { + private val okHttp by lazy { OkHttpClient.Builder().build() } + + private val json = Json { + encodeDefaults = false + ignoreUnknownKeys = true + } + + fun prescribeToPatient(kvNr: String): Task { + val payload = defaultPayload(kvNr) + + val response = okHttp.newCall( + Request.Builder() + .url("${TestConfig.FD.DefaultServer}/doc/${TestConfig.FD.DefaultDoctor}/prescribe") + .post(payload.toRequestBody("application/json".toMediaType())) + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + + val body = requireNotNull(response.body).string() + Log.d("prescribeToPatient", body) + + return json.decodeFromString(body) + } + + fun prescriptionDetails(taskId: String): Prescription { + val response = okHttp.newCall( + Request.Builder() + .url("${TestConfig.FD.DefaultServer}/prescription/$taskId") + .get() + .build() + ).execute() + + require(response.isSuccessful) + + val body = requireNotNull(response.body).string() + Log.d("prescriptionDetails", body) + + return json.decodeFromString(body) + } +} + +fun retry(n: Int, block: () -> T): T? { + var retriesLeft = n + while (retriesLeft > 0) { + try { + return block() + } catch (e: Exception) { + Log.e("retry", "Reason: ", e) + retriesLeft-- + } + } + return null +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt new file mode 100644 index 00000000..5a4ef7c5 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionPharmacyUseCase.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import android.util.Log +import de.gematik.ti.erp.app.test.test.TestConfig +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +class PrescriptionPharmacyUseCase { + private val okHttp by lazy { OkHttpClient.Builder().build() } + + private val json = Json { + encodeDefaults = false + ignoreUnknownKeys = true + } + fun rejectTask(taskId: String, accessCode: String, secret: String) { + val response = okHttp.newCall( + @Suppress("ktlint:max-line-length", "MaxLineLength") + Request.Builder() + .url( + "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/reject/?taskId=$taskId&ac=$accessCode&secret=$secret" + ) + .post("".toRequestBody()) + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + } + + fun acceptTask(taskId: String, accessCode: String): Task { + val response = okHttp.newCall( + @Suppress("ktlint:max-line-length", "MaxLineLength") + Request.Builder() + .url( + "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/accept/?taskId=$taskId&ac=$accessCode" + ) + .post("".toRequestBody()) + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + + val body = requireNotNull(response.body).string() + Log.d("acceptTask", body) + + return json.decodeFromString(body) + } + + fun abortTask(taskId: String, accessCode: String, secret: String) { + val response = okHttp.newCall( + @Suppress("ktlint:max-line-length", "MaxLineLength") + Request.Builder() + .url( + "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/abort/?taskId=$taskId&ac=$accessCode&secret=$secret" + ) + .delete() + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + } + + fun replyWithCommunication(taskId: String, kvNr: String, message: CommunicationPayloadInbox) { + val supplyOption = when (message.supplyOptionsType) { + SupplyOptionsType.Shipment -> "shipment" + SupplyOptionsType.OnPremise -> "onPremise" + SupplyOptionsType.Delivery -> "delivery" + } + + val response = okHttp.newCall( + @Suppress("ktlint:max-line-length", "MaxLineLength") + Request.Builder() + .url( + "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/reply/?taskId=$taskId&kvnr=$kvNr&supplyOption=$supplyOption" + ) + .post(json.encodeToString(message).toRequestBody()) + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + } + + fun dispenseMedication(taskId: String, accessCode: String, secret: String) { + val response = okHttp.newCall( + @Suppress("ktlint:max-line-length", "MaxLineLength") + Request.Builder() + .url( + "${TestConfig.FD.DefaultServer}/pharm/${TestConfig.FD.DefaultPharmacy}/close/?taskId=$taskId&ac=$accessCode&secret=$secret" + ) + .delete() + .build() + ).execute() + + require(response.isSuccessful) { "Response was: $response" } + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt new file mode 100644 index 00000000..ecea4731 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/PrescriptionUtils.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.core.sleep +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen + +class PrescriptionUtils( + private val composeRule: AndroidComposeTestRule, MainActivity>, + private val mainScreen: MainScreen, + private val prescriptionsScreen: PrescriptionsScreen +) { + fun loginWithVirtualHealthCardFromMainScreen() { + mainScreen.userSeesMainScreen() + composeRule.activity.testWrapper.loginWithVirtualHealthCard() + composeRule.sleep(1_000L) + prescriptionsScreen.refreshPrescriptions() + } + + fun deleteAllPrescriptions() { + composeRule.activity.testWrapper.deleteAllTasksSafe() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt new file mode 100644 index 00000000..64155211 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Task.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +@Serializable +data class Task +@OptIn(ExperimentalSerializationApi::class) +constructor( + @SerialName("task-id") + val taskId: String, + @JsonNames("access-code", "accessCode") + val accessCode: String, + @SerialName("secret") + val secret: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt new file mode 100644 index 00000000..49bbaabb --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/core/prescription/Telematik.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.core.prescription + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Telematik( + @SerialName("discoveryDocument") + val discoveryDocument: String? = null, + @SerialName("fachdienst") + val fachdienst: String? = null, + @SerialName("tsl") + val tsl: String? = null +) diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt new file mode 100644 index 00000000..9d29e0af --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/functions/ReadyPrescriptionStateInfoTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.functions + +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.platform.app.InstrumentationRegistry +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.ui.PrescriptionStateInfo +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.toStartOfDayInUTC +import kotlinx.datetime.Clock +import org.junit.Rule +import org.junit.Test +import kotlin.time.Duration.Companion.days + +class ReadyPrescriptionStateInfoTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val now = Clock.System.now() + + @Test + fun syncedTaskReadyState_27AcceptDaysLeftTest() { + val acceptUntil = now.plus(27.days).toStartOfDayInUTC() + val expiresOn = now.plus(90.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = String.format(context.resources.getString(R.string.prescription_item_accept_days_left), "27") + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_2AcceptDaysLeftTest() { + val acceptUntil = now.plus(2.days).toStartOfDayInUTC() + val expiresOn = now.plus(65.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + + String.format(context.resources.getString(R.string.prescription_item_two_accept_days_left), "2") + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_1AcceptDaysLeftTest() { + val acceptUntil = now.plus(1.days).toStartOfDayInUTC() + val expiresOn = now.plus(64.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + + context.resources.getString(R.string.prescription_item_accept_only_tomorrow) + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_0AcceptDaysLeftTest() { + val acceptUntil = now.toStartOfDayInUTC() + val expiresOn = now.plus(63.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = String.format(context.resources.getString(R.string.prescription_item_warning_amber)) + + context.resources.getString(R.string.prescription_item_accept_only_today) + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_62ExpiryDaysLeftTest() { + val acceptUntil = now.minus(1.days).toStartOfDayInUTC() + val expiresOn = now.plus(62.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = + String.format(context.resources.getString(R.string.prescription_item_expiration_days_left), "62") + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_2ExpiryDaysLeftTest() { + val acceptUntil = now.minus(61.days).toStartOfDayInUTC() + val expiresOn = now.plus(2.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + + String.format(context.resources.getString(R.string.prescription_item_two_expiration_days_left), "2") + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_1ExpiryDaysLeftTest() { + val acceptUntil = now.minus(62.days).toStartOfDayInUTC() + val expiresOn = now.plus(1.days).toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + + context.resources.getString(R.string.prescription_item_expiration_only_tomorrow) + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } + + @Test + fun syncedTaskReadyState_0ExpiryDaysLeftTest() { + val acceptUntil = now.minus(63.days).toStartOfDayInUTC() + val expiresOn = now.toStartOfDayInUTC() + val readyState = SyncedTaskData.SyncedTask.Ready(acceptUntil = acceptUntil, expiresOn = expiresOn) + val expectedText = context.resources.getString(R.string.prescription_item_warning_amber) + + context.resources.getString(R.string.prescription_item_expiration_only_today) + composeTestRule.setContent { + AppTheme { + PrescriptionStateInfo(state = readyState) + } + } + composeTestRule.onNode(hasText(expectedText)).assertExists() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt new file mode 100644 index 00000000..2da55556 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Cleanup.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen +import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen +import org.junit.Assume +import org.junit.Rule +import org.junit.Test + +class Cleanup { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } + private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } + + @Test + fun not_a_test_cleanup() { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + prescriptionsScreen.awaitPrescriptions() + prescriptionUtils.deleteAllPrescriptions() + Assume.assumeTrue(false) + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt new file mode 100644 index 00000000..7ece16ee --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/HealthCardOrder.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.LargeTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.steps.CardWallScreenSteps +import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps +import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps +import de.gematik.ti.erp.app.test.test.steps.SettingScreenSteps +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@LargeTest +class HealthCardOrder(fontScale: String) : WithFontScale(fontScale) { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingSteps by lazy { OnboardingSteps(composeRule) } + private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } + private val cardWallScreenSteps by lazy { CardWallScreenSteps(composeRule) } + + private val settingScreenSteps by lazy { SettingScreenSteps(composeRule) } + + @Before + fun skipsOnboarding() { + onboardingSteps.userSkipsOnboarding() + } + + @After + fun destroyActivity() { + composeRule.activity.finish() + } + + @Test + fun list_health_insurance_company() { + mainScreenSteps.userTapsSettingsMenuButton() + settingScreenSteps.userWantsToOrderNewCard() + settingScreenSteps.userSeesAListOfInsurances() + } + + @Test + fun ordering_new_EGKFromSettings() { + mainScreenSteps.userTapsSettingsMenuButton() + settingScreenSteps.userWantsToOrderNewCard() + settingScreenSteps.userSeesAListOfInsurances() + settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") + settingScreenSteps.userSeesOrderOptionScreen() + settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") + settingScreenSteps.userSeesHealthCardOrderContactScreen() + settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") + } + + @Test + fun ordering_new_EGKFromIntroScreen() { + cardWallScreenSteps.fakeNFCCapabilities() + mainScreenSteps.userTapsConnect() + cardWallScreenSteps.userClicksOrderHealthCardFromCardWallIntroScreen() + settingScreenSteps.userSeesAListOfInsurances() + settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") + settingScreenSteps.userSeesOrderOptionScreen() + settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") + settingScreenSteps.userSeesHealthCardOrderContactScreen() + settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") + } + + @Test + fun ordering_new_EGKFromCANScreen() { + cardWallScreenSteps.fakeNFCCapabilities() + mainScreenSteps.userTapsConnect() + cardWallScreenSteps.userClicksOrderHealthCardFromCardWallCANScreen() + settingScreenSteps.userSeesAListOfInsurances() + settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") + settingScreenSteps.userSeesOrderOptionScreen() + settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") + settingScreenSteps.userSeesHealthCardOrderContactScreen() + settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") + } + + @Test + fun ordering_new_EGKFromPINScreen() { + cardWallScreenSteps.fakeNFCCapabilities() + mainScreenSteps.userTapsConnect() + cardWallScreenSteps.userClicksOrderHealthCardFromCardWallPinScreen() + settingScreenSteps.userSeesAListOfInsurances() + settingScreenSteps.userChoosesInsurance("AOK - Die Gesundheitskasse Hessen") + settingScreenSteps.userSeesOrderOptionScreen() + settingScreenSteps.userSeesPossibilitiesWhatCanBeOrdered("Karten & PIN, Nur PIN") + settingScreenSteps.userSeesHealthCardOrderContactScreen() + settingScreenSteps.userSeesPossibilitiesHowCanBeOrdered("Webseite") + } + + @Test + fun cancel_ordering_new_EGK() { + mainScreenSteps.userTapsSettingsMenuButton() + settingScreenSteps.userWantsToOrderNewCard() + settingScreenSteps.userSeesAListOfInsurances() + settingScreenSteps.userAbortsOrderingOfNewCard() + settingScreenSteps.userSeesSettingsScreen() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfile.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfile.kt new file mode 100644 index 00000000..9fd16bd8 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/MultiProfile.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.MediumTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps +import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps +import de.gematik.ti.erp.app.test.test.steps.ProfileSettingsSteps +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@MediumTest +class MultiProfile(fontScale: String) : WithFontScale(fontScale) { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingSteps by lazy { OnboardingSteps(composeRule) } + private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } + + private val profileSettingSteps by lazy { ProfileSettingsSteps(composeRule) } + + @Before + fun skipsOnboarding() { + onboardingSteps.userSkipsOnboarding() + } + + @Test + fun create_second_profile_and_delete_first_one() { + mainScreenSteps.createNewProfile("Herbert Hepatitis B") + profileSettingSteps.userHasNumberOfProfiles(2) + profileSettingSteps.userDeletesProfile("Profil 1") + profileSettingSteps.userHasProfilesWithName(1, "Herbert Hepatitis B") + } + + @Test + fun delete_last_profile() { + mainScreenSteps.userTapsSettingsMenuButton() + profileSettingSteps.userDeletesProfile("Profil 1") + profileSettingSteps.createProfileAfterLastOneWasDeleted() + profileSettingSteps.userHasProfilesWithName(1, "Profil 1") + } + + @Test + fun abort_delete_last_profile() { + profileSettingSteps.userInterruptsDeletingProfile("Profil 1") + profileSettingSteps.userHasProfilesWithName(1, "Profil 1") + } + + @Test + fun abort_create_new_profile() { + mainScreenSteps.userTapsAddProfileButton() + mainScreenSteps.userCantConfirmCreation() + mainScreenSteps.userTapsAbort() + } + + @Test + fun create_profile_without_name() { + mainScreenSteps.userTapsAddProfileButton() + mainScreenSteps.userCantConfirmCreation() + mainScreenSteps.userTapsAbort() + profileSettingSteps.userHasNumberOfProfiles(1) + } + + @Test + fun delete_name_of_a_profile() { + profileSettingSteps.editProfileName("Profil 1", "") + profileSettingSteps.assertErrorMessageEmptyProfileName("Das Namensfeld darf nicht leer sein.") + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt new file mode 100644 index 00000000..e8177cce --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV1.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.SmallTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps +import org.junit.Rule +import org.junit.Test + +@SmallTest +class OnboardingV1(fontScale: String) : WithFontScale(fontScale) { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingSteps by lazy { OnboardingSteps(composeRule) } + private fun restartApp() { + composeRule.activityRule.scenario.recreate() + } + + @Test + fun first_time() { + // Onboarding wird beim 1. App-Start angezeigt + onboardingSteps.userSeesWelcomeScreen() + } + + @Test + fun shown_again_if_not_finished() { + // Onboarding wird nach App-Neustart weiterhin angezeigt, solange es noch nicht beendet wurde + + onboardingSteps.userSeesWelcomeScreen() + restartApp() + onboardingSteps.userSeesWelcomeScreen() + } + + @Test + fun navigate_through_onboarding_without_analytics() { + // welcome screen + onboardingSteps.userSeesWelcomeScreen() + onboardingSteps.userIsFinishingTheOnboardingWithoutAnalytics() + } + + @Test + fun navigate_through_analytics_optional() { + // welcome screen + onboardingSteps.userSeesWelcomeScreen() + onboardingSteps.userIsFinishingTheOnboardingWithAnalytics() + } + + @Test + fun navigate_through_and_restart_app() { + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.MainScreen) + + restartApp() + + onboardingSteps.userIsNotSeeingTheOnboarding() + onboardingSteps.userSeesMainScreen() + } + + @Test + fun weak_password_blocks_next() { + // Onboarding Screen 3 >>> Zu schwaches Passwort = Weiter-Button inaktiv + Fehlermeldung + + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.Credentials) + // credentials screen + onboardingSteps.userSeesCredentialScreen() + + onboardingSteps.userEntersAWeakPasswordTwice() + + onboardingSteps.userDoesNotSeeContinueButton() + onboardingSteps.userSeesErrorMessageForPasswordStrength() + } + + @Test + fun no_password_blocks_next() { + // Onboarding Screen 3 >>> kein Passwort = Weiter-Button inaktiv + Fehlermeldung + + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.Credentials) + // credentials screen + onboardingSteps.userSeesCredentialScreen() + + onboardingSteps.userSwitchesToPasswordMode() + + onboardingSteps.userDoesNotSeeContinueButton() + onboardingSteps.userSeesErrorMessageForPasswordStrength() + } + + @Test + fun not_confirming_data_protection_blocks_next() { + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) + onboardingSteps.dataTermsSwitchDeactivated() + onboardingSteps.confirmContinueButtonIsDeactivated() + onboardingSteps.toggleDataTermsSwitch() + onboardingSteps.userSeesActivatedContinueButton() + onboardingSteps.toggleDataTermsSwitch() + onboardingSteps.confirmContinueButtonIsDeactivated() + } + + @Test + fun data_protection_can_be_read() { + // Datenschutzbestimmungen werden angezeigt und können geschlossen werden + + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) + + onboardingSteps.userDoesntSeeDataProtection() + onboardingSteps.userOpensDataProtection() + onboardingSteps.userSeesDataProtection() + onboardingSteps.userClosesDataProtection() + onboardingSteps.userDoesntSeeDataProtection() + } + + @Test + fun terms_of_use_can_be_read() { + // Nutzungsbedingungen werden angezeigt und können geschlossen werden + + onboardingSteps.userNavigatesToOnboardingScreenName(OnboardingSteps.Page.DataTerms) + + onboardingSteps.userSeesNoTermsOfUse() + onboardingSteps.userOpensTermsOfUse() + onboardingSteps.userSeesTermsOfUse() + onboardingSteps.userClosesTermsOfUse() + onboardingSteps.userSeesNoTermsOfUse() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt new file mode 100644 index 00000000..414e6440 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/OnboardingV2.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.SmallTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen +import org.junit.Rule +import org.junit.Test + +@SmallTest +class OnboardingV2(fontScale: String) : WithFontScale(fontScale) { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + + private fun restartApp() { + composeRule.activityRule.scenario.recreate() + } + + @Test + fun first_time() { + // Onboarding wird beim 1. App-Start angezeigt + + onboardingScreen.checkWelcomePageIsPresent() + } + + @Test + fun shown_again_if_not_finished() { + // Onboarding wird nach App-Neustart weiterhin angezeigt, solange es noch nicht beendet wurde + + onboardingScreen.checkWelcomePageIsPresent() + restartApp() + onboardingScreen.checkWelcomePageIsPresent() + } + + @Test + fun navigate_through() { + // welcome screen + onboardingScreen.checkWelcomePageIsPresent() + onboardingScreen.waitForSecondOnboardingPage() + + // data terms screen + onboardingScreen.checkDataTermsPageIsPresent() + + onboardingScreen.checkDataTermsSwitchDeactivated() + onboardingScreen.checkContinueTutorialButtonIsDeactivated() + + onboardingScreen.tapDataTermsSwitch() + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.tapContinueButton() + + // credentials screen + onboardingScreen.checkCredentialsPageIsPresent() + + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.StrongPassword) + onboardingScreen.enterPasswordB(TestConfig.StrongPassword) + onboardingScreen.tapContinueButton() + + // analytics screen + onboardingScreen.checkAnalyticsPageIsPresent() + + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.checkAnalyticsSwitchIsDeactivated() + + onboardingScreen.toggleAnalyticsSwitch() + onboardingScreen.waitForAnalyticsPage() + onboardingScreen.tapAcceptAnalyticsButton() + + onboardingScreen.checkAnalyticsSwitchIsActivated() + + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.tapContinueButton() + + // main screen + mainScreen.userSeesMainScreen() + } + + @Test + fun navigate_through_analytics_optional() { + // welcome screen + onboardingScreen.checkWelcomePageIsPresent() + onboardingScreen.waitForSecondOnboardingPage() + + // data terms screen + onboardingScreen.checkDataTermsPageIsPresent() + + onboardingScreen.checkDataTermsSwitchDeactivated() + onboardingScreen.checkContinueTutorialButtonIsDeactivated() + + onboardingScreen.tapDataTermsSwitch() + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.tapContinueButton() + + // credentials screen + onboardingScreen.checkCredentialsPageIsPresent() + + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.StrongPassword) + onboardingScreen.enterPasswordB(TestConfig.StrongPassword) + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.tapContinueButton() + + // analytics screen + onboardingScreen.checkAnalyticsPageIsPresent() + + onboardingScreen.checkContinueTutorialButtonIsEnabled() + onboardingScreen.checkAnalyticsSwitchIsDeactivated() + + onboardingScreen.tapContinueButton() + + // main screen + mainScreen.userSeesMainScreen() + } + + private enum class Page { + DataTerms, Credentials, Analytics, MainScreen + } + + @Suppress("ReturnCount") + private fun walkThroughOnboardingWithoutChecks(until: Page? = null) { + // welcome screen + onboardingScreen.checkWelcomePageIsPresent() + onboardingScreen.waitForSecondOnboardingPage() + + if (until == Page.DataTerms) return + + // data terms screen + onboardingScreen.checkDataTermsPageIsPresent() + + onboardingScreen.tapDataTermsSwitch() + + onboardingScreen.tapContinueButton() + + if (until == Page.Credentials) return + + // credentials screen + onboardingScreen.checkCredentialsPageIsPresent() + + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.StrongPassword) + onboardingScreen.enterPasswordB(TestConfig.StrongPassword) + onboardingScreen.checkContinueTutorialButtonIsEnabled() + + onboardingScreen.tapContinueButton() + + if (until == Page.Analytics) return + + // analytics screen + onboardingScreen.checkAnalyticsPageIsPresent() + + onboardingScreen.tapContinueButton() + + if (until == Page.MainScreen) return + + // main screen + mainScreen.userSeesMainScreen() + } + + @Test + fun navigate_through_and_restart_app() { + walkThroughOnboardingWithoutChecks() + + restartApp() + + mainScreen.userSeesMainScreen() + } + + @Test + fun weak_password_blocks_next() { + // Onboarding Screen 3 >>> Zu schwaches Passwort = Weiter-Button inaktiv + Fehlermeldung + + walkThroughOnboardingWithoutChecks(until = Page.Credentials) + + // credentials screen + onboardingScreen.checkCredentialsPageIsPresent() + + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.WeakPassword) + onboardingScreen.enterPasswordB(TestConfig.WeakPassword) + + onboardingScreen.checkContinueTutorialButtonIsDisabled() + onboardingScreen.checkPasswordErrorMessagePresent() + } + + @Test + fun data_protection_can_be_read() { + // Datenschutzbestimmungen werden angezeigt und können geschlossen werden + + walkThroughOnboardingWithoutChecks(until = Page.DataTerms) + + onboardingScreen.checkDataTermsPageIsPresent() + onboardingScreen.checkDataProtectionAreNotDisplayed() + onboardingScreen.openDataProtection() + onboardingScreen.checkDataProtectionIsDisplayed() + onboardingScreen.closeDataProtection() + onboardingScreen.checkDataProtectionAreNotDisplayed() + } + + @Test + fun terms_of_use_can_be_read() { + // Nutzungsbedingungen werden angezeigt und können geschlossen werden + + walkThroughOnboardingWithoutChecks(until = Page.DataTerms) + + onboardingScreen.checkDataTermsPageIsPresent() + onboardingScreen.checkTermsOfUseAreNotDisplayed() + onboardingScreen.openTermsOfUse() + onboardingScreen.checkTermsOfUseAreDisplayed() + onboardingScreen.closeTermsOfUse() + onboardingScreen.checkTermsOfUseAreNotDisplayed() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt new file mode 100644 index 00000000..f96e2fc1 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/Orders.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.espresso.intent.rule.IntentsRule +import androidx.test.filters.LargeTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.core.TaskCollection +import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox +import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils +import de.gematik.ti.erp.app.test.test.core.prescription.SupplyOptionsType +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen +import de.gematik.ti.erp.app.test.test.screens.PharmacySearchScreen +import de.gematik.ti.erp.app.test.test.screens.PrescriptionOrderScreen +import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen +import org.junit.Rule +import org.junit.Test + +@LargeTest +class Orders(fontScale: String) : WithFontScale(fontScale) { + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + @get:Rule(order = 0) + val intentsRule = IntentsRule() + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } + private val pharmacySearchScreen by lazy { PharmacySearchScreen(composeRule) } + private val prescriptionOrderScreen by lazy { PrescriptionOrderScreen(composeRule) } + private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } + + @Test + fun order_prescription() { + val tasks = TaskCollection.generate( + 1, + TestConfig.AppDefaultVirtualEgkKvnr, + composeRule.activity.testWrapper + ) + + fun firstTask() = tasks.taskData.first() + + try { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + prescriptionsScreen.awaitPrescriptions() + + prescriptionsScreen.userSeesPZNPrescription( + data = firstTask().prescription, + inState = PrescriptionsScreen.PrescriptionState.Redeemable + ) + + mainScreen.userClicksBottomBarPharmacy() + + pharmacySearchScreen.userSeesPharmacyOverviewScreen() + + pharmacySearchScreen.userClicksSearchButton() + pharmacySearchScreen.userSeesPharmacySearchResultScreen() + pharmacySearchScreen.userSearchesForTestPharmacy() + pharmacySearchScreen.awaitSearchResults() + pharmacySearchScreen.userClicksOnTestPharmacy() + + pharmacySearchScreen.userSeesPharmacyOrderOptions() + pharmacySearchScreen.awaitOrderOptionsEnabled() + pharmacySearchScreen.userClicksOnOrderByPickUp() + + pharmacySearchScreen.userSeesPharmacyOrderSummaryScreen() + + // all contact information should be available; therefore the order button is enabled + pharmacySearchScreen.userSeesSendOrderButtonEnabled() + + pharmacySearchScreen.userClicksPrescriptionSelection() + pharmacySearchScreen.userSeesPrescriptionSelectionScreen() + pharmacySearchScreen.userDeselectsAllPrescriptions() + pharmacySearchScreen.userSelectsPrescription(firstTask().prescription) + pharmacySearchScreen.userClicksBack() + pharmacySearchScreen.userSeesPharmacyOrderSummaryScreen() + + pharmacySearchScreen.userSeesSendOrderButtonEnabled() + pharmacySearchScreen.userClicksSendOrderButton() + + mainScreen.userSeesMainScreen(10_000L) + prescriptionsScreen.refreshPrescriptions() + + prescriptionsScreen.userSeesPZNPrescription( + data = firstTask().prescription, + inState = PrescriptionsScreen.PrescriptionState.WaitForResponse + ) + + // pharmacy accepts task + tasks.accept(firstTask()) + val message = CommunicationPayloadInbox( + supplyOptionsType = SupplyOptionsType.Delivery, + url = "https://www.whatever.de/blablub", + infoText = "Hey u!", + pickUpCodeHR = "a1234567890", + pickUpCodeDMC = "b1234567890" + ) + tasks.reply(firstTask(), message) + + prescriptionsScreen.refreshPrescriptions() + + prescriptionsScreen.userSeesPZNPrescription( + data = firstTask().prescription, + inState = PrescriptionsScreen.PrescriptionState.InProgress + ) + + mainScreen.userClicksBottomBarOrders() + + prescriptionOrderScreen.userSeesOrderScreen() + prescriptionOrderScreen.awaitOrders() + + prescriptionOrderScreen.userClicksNewestOrder() + prescriptionOrderScreen.userSeesOrderDetailsScreen() + prescriptionOrderScreen.userSeesOnePrescription(firstTask().prescription) + prescriptionOrderScreen.userSeesAndClicksMessage() + prescriptionOrderScreen.userExpectsMessageContent(message) + prescriptionOrderScreen.userClicksMessageLink(message) + + prescriptionOrderScreen.userClosesMessageSheetBySwipe() + + prescriptionOrderScreen.userClicksPrescription(firstTask().prescription) + prescriptionsScreen.userSeesPrescriptionDetails() + prescriptionsScreen.userClicksClose() + prescriptionOrderScreen.userClicksBack() + + // dispense medication + tasks.dispense(firstTask()) + + mainScreen.userClicksBottomBarPrescriptions() + + prescriptionsScreen.awaitPrescriptionScreen() + prescriptionsScreen.refreshPrescriptions() + + prescriptionsScreen.userMissesPrescription(firstTask().prescription) + prescriptionsScreen.userClicksArchiveButton() + prescriptionsScreen.awaitArchivedPrescriptions() + prescriptionsScreen.userSeesPrescriptionInArchive( + data = firstTask().prescription, + inState = PrescriptionsScreen.PrescriptionState.Redeemed + ) + } finally { + tasks.deleteAll() + } + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt new file mode 100644 index 00000000..d4d51f79 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PharmacyUITest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.sharedtest.testresources.actions.PharmacyScreenAction +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PharmacyUITest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val actions = PharmacyScreenAction(composeRule) + + @Test + fun pickupServiceSuccessTest() { + actions.pickupServiceSuccessTest() + } + + @Test + fun courierDeliverySuccessTest() { + actions.courierDeliverySuccessTest() + } + + @Test + fun pickupServiceMailDeliverySuccessTest() { + actions.pickupServiceMailDeliverySuccessTest() + } + + @Test + fun mailDeliverySuccessTest() { + actions.mailDeliverySuccessTest() + } + + @Test + fun pickupServiceCourierSuccessTest() { + actions.pickupServiceCourierSuccessTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { + actions.pickupServiceMailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun mailDeliveryCourierDeliverySuccessTest() { + actions.mailDeliveryCourierDeliverySuccessTest() + } + + @Test + fun pickupServiceFailTest() { + actions.pickupServiceFailTest() + } + + @Test + fun courierDeliveryFailTest() { + actions.courierDeliveryFailTest() + } + + @Test + fun mailDeliveryFailTest() { + actions.mailDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryCourierDeliveryFailTest() { + actions.pickupServiceMailDeliveryCourierDeliveryFailTest() + } + + @Test + fun pickupServiceMailDeliveryFailTest() { + actions.pickupServiceMailDeliveryFailTest() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetails.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetails.kt new file mode 100644 index 00000000..031c395a --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/PrescriptionDetails.kt @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.LargeTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.core.prescription.PrescriptionUtils +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.core.TaskCollection +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen +import de.gematik.ti.erp.app.test.test.screens.PrescriptionsScreen +import org.junit.Rule +import org.junit.Test + +@LargeTest +class PrescriptionDetails(fontScale: String) : WithFontScale(fontScale) { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + private val prescriptionsScreen by lazy { PrescriptionsScreen(composeRule) } + private val prescriptionUtils by lazy { PrescriptionUtils(composeRule, mainScreen, prescriptionsScreen) } + + @Test + fun pzn_medication_details() { + val tasks = TaskCollection.generate(1, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) + val prescriptionData = tasks.taskData.first().prescription + + try { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + prescriptionsScreen.awaitPrescriptions() + + // details main page + prescriptionsScreen.clickOnPrescription(prescriptionData) + prescriptionsScreen.userSeesPrescriptionDetails() + prescriptionsScreen.userExpectsPrescriptionData(prescriptionData) + + // technical details + prescriptionsScreen.userClicksOnTechnicalDetails() + prescriptionsScreen.userSeesTechnicalDetailsScreen() + prescriptionsScreen.userExpectsTechnicalInformationData(prescriptionData) + + prescriptionsScreen.userClicksBack() + prescriptionsScreen.userSeesPrescriptionDetails() + + // patient page + prescriptionsScreen.userClicksOnPatientDetails() + prescriptionsScreen.userSeesPatientDetailsScreen() + prescriptionsScreen.userExpectsPatientDetailsData(prescriptionData) + + prescriptionsScreen.userClicksBack() + prescriptionsScreen.userSeesPrescriptionDetails() + + // organization page + prescriptionsScreen.userClicksOnOrganizationDetails() + prescriptionsScreen.userSeesOrganizationDetailsScreen() + prescriptionsScreen.userExpectsOrganizationDetailsData(prescriptionData) + + prescriptionsScreen.userClicksBack() + prescriptionsScreen.userSeesPrescriptionDetails() + + // medication page + prescriptionsScreen.userClicksOnMedicationDetails() + prescriptionsScreen.userSeesMedicationDetailsScreen() + prescriptionsScreen.userExpectsMedicationDetailsData(prescriptionData) + + prescriptionsScreen.userClicksBack() + prescriptionsScreen.userSeesPrescriptionDetails() + } finally { + tasks.deleteAll() + } + } + + @Test + fun delete_task() { + val tasks = TaskCollection.generate(1, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) + val prescriptionData = tasks.taskData.first().prescription + + try { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + prescriptionsScreen.awaitPrescriptions() + + // details main page + prescriptionsScreen.clickOnPrescription(prescriptionData) + prescriptionsScreen.userSeesPrescriptionDetails() + + prescriptionsScreen.userClicksMoreButton() + prescriptionsScreen.userClicksDeleteButton() + prescriptionsScreen.userSeesConfirmDeleteDialog() + prescriptionsScreen.userConfirmsDeletion() + + mainScreen.userSeesMainScreen() + prescriptionsScreen.awaitPrescriptions() + + prescriptionsScreen.userMissesPrescription(prescriptionData) + } finally { + tasks.deleteAll() + } + } + + @Test + fun main_screen_with_many_prescriptions() { + val tasks = TaskCollection.generate(6, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) + + try { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + + tasks.taskData.forEach { data -> + prescriptionsScreen.awaitPrescriptions() + prescriptionsScreen.clickOnPrescription(data.prescription) + prescriptionsScreen.userSeesPrescriptionDetails() + + // technical details + prescriptionsScreen.userClicksOnTechnicalDetails() + prescriptionsScreen.userSeesTechnicalDetailsScreen() + prescriptionsScreen.userExpectsTechnicalInformationData(data.prescription) + + prescriptionsScreen.userClicksBack() + prescriptionsScreen.userSeesPrescriptionDetails() + prescriptionsScreen.userClicksClose() + mainScreen.userSeesMainScreen() + } + + // TODO use expiresOn and TODO sorting is unstable (uses expiresOn with day accuracy) in app + // val fromOldToNew = tasks.prescriptions.sortedBy { it.authoredOn } + // prescriptionsScreen.userSeesPrescriptionSortedBy(fromOldToNew) + } finally { + tasks.deleteAll() + } + } + + @Test + fun main_screen_check_if_prescriptions_exist() { + val tasks = TaskCollection.generate(6, TestConfig.AppDefaultVirtualEgkKvnr, composeRule.activity.testWrapper) + + try { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.tapTooltips() + prescriptionUtils.loginWithVirtualHealthCardFromMainScreen() + prescriptionsScreen.awaitPrescriptions() + + tasks.taskData.forEach { data -> + prescriptionsScreen.userSeesPZNPrescription(data.prescription) + } + } finally { + tasks.deleteAll() + } + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileSettings.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileSettings.kt new file mode 100644 index 00000000..29838055 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/scenarios/ProfileSettings.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.scenarios + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.filters.MediumTest +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.test.test.WithFontScale +import de.gematik.ti.erp.app.test.test.core.sleep +import de.gematik.ti.erp.app.test.test.steps.CardWallScreenSteps +import de.gematik.ti.erp.app.test.test.steps.MainScreenSteps +import de.gematik.ti.erp.app.test.test.steps.OnboardingSteps +import de.gematik.ti.erp.app.test.test.steps.ProfileSettingsSteps +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@MediumTest +class ProfileSettings(fontScale: String) : WithFontScale(fontScale) { + + @get:Rule + val composeRule = createAndroidComposeRule() + + private val onboardingSteps by lazy { OnboardingSteps(composeRule) } + private val mainScreenSteps by lazy { MainScreenSteps(composeRule) } + private val profileSettingsSteps by lazy { ProfileSettingsSteps(composeRule) } + private val cardWallScreenSteps by lazy { CardWallScreenSteps(composeRule) } + + @Before + fun skipOnboarding() { + onboardingSteps.userSkipsOnboarding() + } + + @Test + fun no_token_in_profile_if_not_logged_in() { + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.checkNoTokenPresent() + profileSettingsSteps.checkTokenHintPresent() + } + + @Test + fun token_in_profile_if_logged_in() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.checkTokenPresent() + profileSettingsSteps.hintTextNotPresent() + } + + @Test + fun no_token_in_profile_after_logged_out() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + profileSettingsSteps.logoutViaProfileSettings() + profileSettingsSteps.noTokenPresentInProfileSettings() + profileSettingsSteps.checkTokenHintPresent( + "Sie erhalten einen Token, wenn Sie am Rezeptdienst angemeldet sind." + ) + } + + @Test + fun login_in_profile_settings_opens_cardwall() { + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.tapLoginButton() + profileSettingsSteps.userSeesCardwallWelcomeScreen() + } + + @Test + fun no_kvnr_in_profile_settings_if_never_logged_in() { + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.checkNoKVNRIsVisible() + } + + @Test + fun kvnr_in_profile_settings_is_visible_if_logged_in() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.checkKVNRIsVisible() + } + + @Test + fun kvnr_in_profile_settings_is_visible_if_logged_out() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + profileSettingsSteps.logoutViaProfileSettings() + profileSettingsSteps.openProfileSettings() + profileSettingsSteps.checkKVNRIsVisible() + } + + @Test + fun if_profile_changed_show_no_kvnr() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + mainScreenSteps.createNewProfile("Karoline Karies") + profileSettingsSteps.openProfileSettingsForCertainProfile(2) + profileSettingsSteps.checkNoKVNRIsVisible() + } + + @Test + fun show_no_access_protocol_screen() { + profileSettingsSteps.userSeesAuditEventsScreen(1) + profileSettingsSteps.userSeesEmptyStateForCertainProfile() + } + + @Test + fun show_no_access_protocol_screen_when_switching_to_logged_out_profile() { + cardWallScreenSteps.userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() + mainScreenSteps.createNewProfile("Karoline Karies") + profileSettingsSteps.userSeesAuditEventsScreen(2) + profileSettingsSteps.userSeesEmptyStateForCertainProfile() + } + + @Test + fun show_protocol_after_logout() { + cardWallScreenSteps.setVirtualEGKWithPrescriptions() + composeRule.sleep(5_000L) // await audit events + profileSettingsSteps.userLogsOutOffProfile(1) + profileSettingsSteps.userSeesAuditEventsScreen(1) + profileSettingsSteps.userDoesNotSeesEmptyStateForCertainProfile() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt new file mode 100644 index 00000000..931d8605 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/AuditEventsScreen.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class AuditEventsScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesAuditEventsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.AuditEvents.AuditEventsScreen) + } + + fun checkAuditEventsDoNotExist() { + onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventHeader) + .assertIsDisplayed() + onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventInfo) + .assertIsDisplayed() + } + + fun checkAuditEventsExist() { + onAllNodesWithTag(TestTag.Profile.AuditEvents.AuditEvent)[0] + .assertIsDisplayed() + } + + fun checkNoAuditEventsHeaderAndInfoDoesNotExist() { + onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventHeader) + .assertDoesNotExist() + onNodeWithTag(TestTag.Profile.AuditEvents.NoAuditEventInfo) + .assertDoesNotExist() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt new file mode 100644 index 00000000..a65906b8 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/CardWallScreen.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class CardWallScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesIntroScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.Intro.IntroScreen) + } + fun userSeesPinScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.PIN.PinScreen) + } + fun userSeesCANScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.CardWall.CAN.CANScreen) + } + + // ****** LoginScreen ****** + fun continueWithEGK() { + onNodeWithTag(TestTag.CardWall.ContinueButton) + .assertIsDisplayed() + .performClick() + } + + // ****** CAN and PIN Screen ****** + fun enterCAN() { + // CAN + onNodeWithTag(TestTag.CardWall.CAN.CANField) + .assertIsDisplayed() + .performTextInput(TestConfig.DefaultEGKCAN) + onNodeWithTag(TestTag.CardWall.ContinueButton) + .assertIsDisplayed() + .performClick() + } + + fun enterPin() { + // PIN + onNodeWithTag(TestTag.CardWall.PIN.PINField) + .assertIsDisplayed() + .performTextInput(TestConfig.DefaultEGKPassword) + onNodeWithTag(TestTag.CardWall.ContinueButton) + .assertIsDisplayed() + .performClick() + } + + fun saveCredentials() { + onNodeWithTag(TestTag.CardWall.StoreCredentials.Save) + .assertIsDisplayed() + .performClick() + onNodeWithTag(TestTag.CardWall.SecurityAcceptance.AcceptButton) + .assertIsDisplayed() + .performClick() + } + + fun dontSaveCredentials() { + onNodeWithTag(TestTag.CardWall.StoreCredentials.DontSave) + .assertIsDisplayed() + .performClick() + onNodeWithTag(TestTag.CardWall.ContinueButton) + .assertIsDisplayed() + .performClick() + } + + fun userSeesNfcScreen() { + onNodeWithTag(TestTag.CardWall.Nfc.NfcScreen) + .assertIsDisplayed() + } + + fun tapOrderEgkFromIntroScreen() { + onNodeWithTag(TestTag.CardWall.Intro.OrderEgkButton) + .assertIsDisplayed() + .performClick() + } + + fun tapOrderEgkFromCANScreen() { + onNodeWithTag(TestTag.CardWall.CAN.OrderEgkButton) + .assertIsDisplayed() + .performClick() + } + + fun tapOrderEgkFromPinScreen() { + onNodeWithTag(TestTag.CardWall.PIN.OrderEgkButton) + .assertIsDisplayed() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt new file mode 100644 index 00000000..5f16b191 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/DebugMenuScreen.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsToggleable +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextReplacement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.VirtualEgk +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class DebugMenuScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesDebugMenuScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.DebugMenu.DebugMenuScreen) + } + + fun waitTillVirtualHealthCardIsSet() { + composeRule.awaitDisplay(10000) { + onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) + .assertIsNotEnabled() + } + composeRule.awaitDisplay(10000) { + onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) + .assertIsEnabled() + } + } + + fun tapSetVirtualCard() { + onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) + .performScrollToNode(hasTestTag(TestTag.DebugMenu.SetVirtualHealthCardButton)) + onNodeWithTag(TestTag.DebugMenu.SetVirtualHealthCardButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + waitTillVirtualHealthCardIsSet() + } + + fun closeDebugMenu() { + onNodeWithTag(TestTag.TopNavigation.CloseButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun fillCustomCertificateAndPrivateKey(virtualEgk: VirtualEgk) { + fillCertificateFieldWith(virtualEgk.certificate) + fillPrivateKeyFieldWith(virtualEgk.privateKey) + } + + fun fillCertificateFieldWith(certificateString: String) { + onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) + .performScrollToNode(hasTestTag(TestTag.DebugMenu.CertificateField)) + onNodeWithTag(TestTag.DebugMenu.CertificateField) + .assertIsDisplayed() + .performTextReplacement(certificateString) + } + + fun fillPrivateKeyFieldWith(privateKeyString: String) { + onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) + .performScrollToNode(hasTestTag(TestTag.DebugMenu.PrivateKeyField)) + onNodeWithTag(TestTag.DebugMenu.PrivateKeyField) + .assertIsDisplayed() + .performTextReplacement(privateKeyString) + } + + fun tapFakeNFCCapabilitiesSwitch() { + onNodeWithTag(TestTag.DebugMenu.DebugMenuContent) + .performScrollToNode(hasTestTag(TestTag.DebugMenu.FakeNFCCapabilities)) + + onNodeWithTag(TestTag.DebugMenu.FakeNFCCapabilities) + .assertIsDisplayed() + .assertIsToggleable() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt new file mode 100644 index 00000000..84fb7a80 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/MainScreen.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeUp +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class MainScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesMainScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreen) + } + + fun userSeesBottomSheet(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreenBottomSheet.Modal) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.Modal).performTouchInput { swipeUp() } + } + + fun checkProfileHasState(profileName: String, profileState: String) { + onNodeWithText(profileName, substring = true) + .assertIsDisplayed() + onNodeWithText(profileState, substring = true) + .assertIsDisplayed() + } + + fun refreshMainScreenBySwipe() { + onNodeWithTag(TestTag.Main.MainScreen) + .assertIsDisplayed() + .performTouchInput { swipeDown() } + } + + fun tapLoginButton() { + onNodeWithTag(TestTag.Main.LoginButton) + .assertIsDisplayed() + .performClick() + } + + fun tapConnectLater() { + userSeesBottomSheet(5000) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ConnectLaterButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapTooltips() { + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + } + + fun tapSettingsButton() { + onNodeWithTag(TestTag.BottomNavigation.SettingsButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksBottomBarPrescriptions() { + onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksBottomBarPharmacy() { + onNodeWithTag(TestTag.BottomNavigation.PharmaciesButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksPharmacySearchBar() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + TestConfig.ScreenChangeTimeout + } + + fun userClicksBottomBarOrders() { + onNodeWithTag(TestTag.BottomNavigation.OrdersButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapAddProfileButton() { + onNodeWithTag(TestTag.Main.AddProfileButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun enterProfileName(name: String) { + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) + .assertIsDisplayed() + .performClick() + .performTextInput(name) + } + + fun tapNewProfileConfirmButton() { + userSeesBottomSheet(5000L) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapCancelAddProfileButton() { + userSeesBottomSheet() + onNodeWithTag(TestTag.Main.LoginButton) // BottomSheet has no CancelButton + .performClick() + } + + fun assertConfirmationCanNotBeClicked() { + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) + .assertIsNotEnabled() + } + + /** + * cancel user login + * click-though all tool-tips + * click on pharmacies bottom button + */ + fun openPharmaciesFromBottomBarFromStart() { + tapConnectLater() + tapTooltips() + userClicksBottomBarPharmacy() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt new file mode 100644 index 00000000..f9136dfb --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OnboardingScreen.kt @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.assertIsToggleable +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.test.swipeUp +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class OnboardingScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun checkTutorialIsNotPresent() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertDoesNotExist() + } + + fun waitForSecondOnboardingPage() { + composeRule.awaitDisplay(5000L, TestTag.Onboarding.DataTermsScreen) + } + + fun waitForAnalyticsPage() { + composeRule.awaitDisplay(5000L, TestTag.Onboarding.Analytics.ScreenContent) + } + + fun tapContinueButton() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .performClick() + } + + fun switchToPasswordMode() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordTab) + .assertIsDisplayed() + .performClick() + } + + fun enterPasswordA(password: String) { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldA) + .assertIsDisplayed() + .performClick() + .assertIsFocused() + .performTextInput(password) + } + + fun enterPasswordB(password: String) { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldB) + .assertIsDisplayed() + .performClick() + .assertIsFocused() + .performTextInput(password) + } + + fun tapDataTermsSwitch() { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .performClick() + } + + fun closeDataProtection() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun checkDataProtectionIsDisplayed() { + onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) + .assertIsDisplayed() + } + + fun openDataProtection() { + onNodeWithTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton) + .assertIsDisplayed() + .performClick() + } + + fun checkDataProtectionAreNotDisplayed() { + onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) + .assertDoesNotExist() + } + + fun closeTermsOfUse() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun openTermsOfUse() { + onNodeWithTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton) + .assertIsDisplayed() + .performClick() + } + + fun checkTermsOfUseAreDisplayed() { + onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) + .assertIsDisplayed() + } + + fun checkTermsOfUseAreNotDisplayed() { + onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) + .assertDoesNotExist() + } + + fun checkNoPasswordErrorMessagePresent() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) + .assertIsDisplayed() + .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "sufficient")) + } + + fun checkPasswordErrorMessagePresent() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) + .assertIsDisplayed() + .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "insufficient")) + } + + fun checkContinueTutorialButtonIsEnabled() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsEnabled() + } + + fun checkContinueTutorialButtonIsDeactivated() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsNotEnabled() + } + + fun checkDataTermsSwitchDeactivated() { + onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOff() + } + + fun checkWelcomePageIsPresent() { + onNodeWithTag(TestTag.Onboarding.WelcomeScreen) + .assertExists() + } + + fun checkCredentialsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.CredentialsScreen) + .assertExists() + } + + fun checkAnalyticsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.AnalyticsScreen) + .assertExists() + } + + fun checkDataTermsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.DataTermsScreen) + .assertExists() + } + + fun swipeToNextTutorialStep() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertIsDisplayed() + .performTouchInput { swipeLeft() } + } + + fun swipeToPreviousTutorialStep() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertIsDisplayed() + .performTouchInput { swipeRight() } + } + + fun checkContinueTutorialButtonIsDisabled() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsNotEnabled() + } + + fun tapSkipOnboardingButton() { + onNodeWithTag(TestTag.Onboarding.SkipOnboardingButton) + .assertIsDisplayed() + .performClick() + } + + fun checkAnalyticsSwitchIsDeactivated() { + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .performScrollTo() + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOff() + } + + fun checkAnalyticsSwitchIsActivated() { + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .performScrollTo() + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOn() + } + + fun toggleAnalyticsSwitch() { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .performClick() + } + + fun tapAcceptAnalyticsButton() { + onNodeWithTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt new file mode 100644 index 00000000..93d2429c --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/OrderEgkScreen.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class OrderEgkScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesOrderEgkScreenScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.OrderEgk.OrderEgkScreen) + } + + fun tapNFCExplanationPageLink() { + onNodeWithTag(TestTag.Settings.OrderEgk.NFCExplanationPageLink) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun checkIfAtLeastFourInsurerIsVisible() { + for (i in 0..3) { + onAllNodesWithTag(TestTag.Settings.InsuranceCompanyList.ListOfInsuranceButtons)[i] + .assertIsDisplayed() + .assertHasClickAction() + } + } + + fun chooseInsurance(insurance: String) { + onNodeWithText(insurance, substring = true) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun checkOrderPossibilities(orderPossibility: String) { + when (orderPossibility) { + "Keine Bestellmöglichkeit" -> { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton) + .assertDoesNotExist() + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderPinButton) + .assertDoesNotExist() + } + "Karten & PIN, Nur PIN" -> { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderPinButton) + .assertIsDisplayed() + .assertHasClickAction() + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.OrderEgkAndPinButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + } + } + + fun checkContactPossibilities(contactPossibility: String) { + if (contactPossibility.contains("Telefon")) { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton) + .assertIsDisplayed() + .assertHasClickAction() + } + if (contactPossibility.contains("Webseite")) { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton) + .assertIsDisplayed() + .assertHasClickAction() + } + if (contactPossibility.contains("Mail")) { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.MailToButton) + .assertIsDisplayed() + .assertHasClickAction() + } + if (contactPossibility.contains("Keine Kontaktmöglichkeit")) { + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.TelephoneButton) + .assertDoesNotExist() + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.WebsiteButton) + .assertDoesNotExist() + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.MailToButton) + .assertDoesNotExist() + onNodeWithTag(TestTag.Settings.ContactInsuranceCompany.NoContactInfoTextBox) + .assertIsDisplayed() + } + } + + fun tapOrderCardAbort() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt new file mode 100644 index 00000000..eeb4c67c --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PharmacySearchScreen.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isSelected +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performImeAction +import androidx.compose.ui.test.performScrollToKey +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextInput +import de.gematik.ti.erp.app.PrescriptionIds +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.TestConfig.WaitTimeout1Sec +import de.gematik.ti.erp.app.test.test.core.awaitDisplay +import de.gematik.ti.erp.app.test.test.core.hasPharmacyId +import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId +import de.gematik.ti.erp.app.test.test.core.prescription.Prescription +import de.gematik.ti.erp.app.test.test.core.sleep + +class PharmacySearchScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesPharmacyOverviewScreen() { + onNodeWithTag(TestTag.PharmacySearch.OverviewScreen, useUnmergedTree = true) + .assertIsDisplayed() + } + + fun userClicksSearchButton() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) + .performClick() + } + + fun userSeesPharmacySearchResultScreen() { + onNodeWithTag(TestTag.PharmacySearch.ResultScreen) + .assertIsDisplayed() + } + + fun userSearchesForTestPharmacy() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchField) + .performClick() + .assertIsFocused() + .performTextInput(TestConfig.PharmacyName) + + onNodeWithTag(TestTag.PharmacySearch.TextSearchField) + .performImeAction() + } + + fun awaitSearchResults() { + composeRule.awaitDisplay(20_000L) { + onNodeWithTag(TestTag.PharmacySearch.ResultContent) + .assertIsDisplayed() + .assert(hasAnyChild(hasTestTag(TestTag.PharmacySearch.PharmacyListEntry))) + } + composeRule.sleep(1_000L) + } + + fun userClicksOnTestPharmacy() { + onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) + .assertIsDisplayed() + .performScrollToNode(hasPharmacyId(TestConfig.PharmacyTelematikId)) + .onChildren() + .filterToOne(hasPharmacyId(TestConfig.PharmacyTelematikId)) + .performClick() + } + + fun userClicksOnPharmacyFromListByName(name: String) { + composeRule.sleep(WaitTimeout1Sec) + onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) + .performScrollToNode(hasAnyDescendant(hasText(name))) + .onChildren() + .filterToOne(hasAnyDescendant(hasText(name))) + .performClick() + } + + fun userSeesPharmacyOrderOptions() { + composeRule.sleep(WaitTimeout1Sec) + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.Content, useUnmergedTree = true) + .assertExists() + } + + fun awaitOrderOptionsEnabled() { + composeRule.sleep(1_000L) + } + + fun dismissOrderOptionsBottomSheet() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchField) + .assertIsDisplayed() + .performClick() + } + + fun userClicksOnOrderByCourierDelivery() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun userClicksOnOrderByPickUp() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun checkToastMessageWhenOrderOptionClicked() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.ComposeToast, useUnmergedTree = true) + .assertExists() + } + + fun checkAndClickNoPrescriptionDialog() { + onNodeWithTag(TestTag.AlertDialog.ConfirmButton).assertIsDisplayed().performClick() + } + + fun userClicksOnOrderByMailDelivery() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun userSeesPharmacyOrderSummaryScreen() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.Screen) + .assertIsDisplayed() + } + + fun userSeesSendOrderButtonEnabled() { + // asynchronous process enabling the button + composeRule.awaitDisplay(1_000L) { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) + .assertIsDisplayed() + .assertIsEnabled() + } + } + + fun userClicksPrescriptionSelection() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton) + .assertIsDisplayed() + .performClick() + } + + fun userSeesPrescriptionSelectionScreen() { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen) + .assertIsDisplayed() + } + + fun userDeselectsAllPrescriptions() { + val prescriptionIds = onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .fetchSemanticsNode() + .config[PrescriptionIds]!! + + prescriptionIds.forEach { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .performScrollToKey("prescription-$it") + .onChildren() + .filterToOne(hasPrescriptionId(it).and(isSelected())) + .performClick() + } + } + + fun userSelectsPrescription(prescription: Prescription) { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .assertIsDisplayed() + .performScrollToKey("prescription-${prescription.taskId}") + .onChildren() + .filterToOne(hasPrescriptionId(prescription.taskId)) + .assertIsNotSelected() + .assertIsDisplayed() + .performClick() + .assertIsSelected() + } + + fun userClicksBack() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun userClicksSendOrderButton() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun openOrderOptionsByPharmacyName(name: String) { + userSeesPharmacyOverviewScreen() + userClicksSearchButton() + awaitSearchResults() + userClicksOnPharmacyFromListByName(name) + userSeesPharmacyOrderOptions() + awaitOrderOptionsEnabled() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt new file mode 100644 index 00000000..808f016b --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionOrderScreen.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToKey +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.core.await +import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId +import de.gematik.ti.erp.app.test.test.core.prescription.CommunicationPayloadInbox +import de.gematik.ti.erp.app.test.test.core.prescription.Prescription +import de.gematik.ti.erp.app.test.test.core.sleep + +class PrescriptionOrderScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesOrderScreen() { + onNodeWithTag(TestTag.Orders.Content) + .assertIsDisplayed() + } + + fun awaitOrders() { + composeRule.await(20_000L) { + onNodeWithTag(TestTag.Orders.Content) + .assertIsDisplayed() + .assert(hasAnyChild(hasTestTag(TestTag.Orders.OrderListItem))) + } + composeRule.sleep(2500L) + } + + fun userClicksNewestOrder() { + onNodeWithTag(TestTag.Orders.Content) + .assertIsDisplayed() + .onChildren() + .filter(hasTestTag(TestTag.Orders.OrderListItem)) + .onFirst() + .assertIsDisplayed() + .performClick() + } + + fun userSeesOrderDetailsScreen() { + onNodeWithTag(TestTag.Orders.Details.Screen) + .assertIsDisplayed() + } + + fun userSeesOnePrescription(prescription: Prescription) { + onNodeWithTag(TestTag.Orders.Details.Content) + .assertIsDisplayed() + .performScrollToKey("prescriptions") + .onChildren() + .filterToOne(hasPrescriptionId(prescription.taskId)) + .assertIsDisplayed() + } + + fun userSeesAndClicksMessage() { + onNodeWithTag(TestTag.Orders.Details.Content) + .assertIsDisplayed() + .performScrollToKey("prescriptions") + .onChildren() + .filterToOne(hasTestTag(TestTag.Orders.Details.MessageListItem)) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userExpectsMessageContent(message: CommunicationPayloadInbox) { + onNodeWithTag(TestTag.Orders.Messages.Content) + .assertIsDisplayed() + + message.infoText?.let { + onNodeWithTag(TestTag.Orders.Messages.Text) + .assertIsDisplayed() + .assertTextContains(message.infoText) + } + ?: onNodeWithTag(TestTag.Orders.Messages.Text).assertDoesNotExist() + + message.url?.let { + onNodeWithTag(TestTag.Orders.Messages.Link) + .assertIsDisplayed() + + onNodeWithTag(TestTag.Orders.Messages.LinkButton) + .assertIsDisplayed() + .assertIsEnabled() + .assertHasClickAction() + } + ?: onNodeWithTag(TestTag.Orders.Messages.Link).assertDoesNotExist() + + if (message.pickUpCodeHR != null || message.pickUpCodeDMC != null) { + onNodeWithTag(TestTag.Orders.Messages.Code) + .assertIsDisplayed() + + if (message.pickUpCodeDMC != null) { + onNodeWithTag(TestTag.Orders.Messages.CodeLabelContent) + .assertTextContains(message.pickUpCodeDMC, substring = true) + } else if (message.pickUpCodeHR != null) { + onNodeWithTag(TestTag.Orders.Messages.CodeLabelContent) + .assertTextContains(message.pickUpCodeHR, substring = true) + } + } else { + onNodeWithTag(TestTag.Orders.Messages.Code).assertDoesNotExist() + } + } + + fun userClicksMessageLink(message: CommunicationPayloadInbox) { + Intents.intending(IntentMatchers.hasData(message.url)) + .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())) + + onNodeWithTag(TestTag.Orders.Messages.LinkButton) + .assertIsDisplayed() + .assertIsEnabled() + .assertHasClickAction() + .performClick() + + Intents.intended(IntentMatchers.hasData(message.url)) + } + + fun userClosesMessageSheetBySwipe() { + onNodeWithTag(TestTag.Orders.Details.Content) + .performTouchInput { + swipeDown() + } + } + + fun userClicksPrescription(prescription: Prescription) { + onNodeWithTag(TestTag.Orders.Details.Content) + .assertIsDisplayed() + .performScrollToKey("prescriptions") + .onChildren() + .filterToOne(hasPrescriptionId(prescription.taskId)) + .assertIsDisplayed() + .performClick() + } + + fun userClicksBack() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt new file mode 100644 index 00000000..a45304bf --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/PrescriptionsScreen.kt @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import android.util.Log +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToKey +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.assertHasText +import de.gematik.ti.erp.app.test.test.core.await +import de.gematik.ti.erp.app.test.test.core.hasInsuranceState +import de.gematik.ti.erp.app.test.test.core.hasMedicationCategory +import de.gematik.ti.erp.app.test.test.core.hasPrescriptionId +import de.gematik.ti.erp.app.test.test.core.hasSubstitutionAllowed +import de.gematik.ti.erp.app.test.test.core.hasSupplyForm +import de.gematik.ti.erp.app.test.test.core.prescription.Prescription +import de.gematik.ti.erp.app.test.test.core.sleep +import org.junit.Assert.assertTrue + +@Suppress("UnusedPrivateMember") +class PrescriptionsScreen(private val composeRule: ComposeTestRule) : SemanticsNodeInteractionsProvider by composeRule { + enum class PrescriptionState { + Redeemable, // ready + WaitForResponse, // artificial state + InProgress, // inProgress + Redeemed + } + + fun userClicksBack() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksClose() { + onNodeWithTag(TestTag.TopNavigation.CloseButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun awaitPrescriptions() { + composeRule.await(TestConfig.LoadPrescriptionsTimeout) { + onNodeWithTag(TestTag.Prescriptions.Content) + .assertIsDisplayed() + .assert(hasAnyChild(hasTestTag(TestTag.Prescriptions.FullDetailPrescription))) + } + composeRule.sleep(2500L) + } + + fun awaitArchivedPrescriptions() { + composeRule.await(TestConfig.LoadPrescriptionsTimeout) { + onNodeWithTag(TestTag.Prescriptions.Archive.Content) + .assertIsDisplayed() + .assert(hasAnyChild(hasTestTag(TestTag.Prescriptions.FullDetailPrescription))) + } + composeRule.sleep(2500L) + } + + fun awaitPrescriptionScreen() { + composeRule.await(TestConfig.ScreenChangeTimeout) { + onNodeWithTag(TestTag.Prescriptions.Content) + .assertIsDisplayed() + } + } + + fun refreshPrescriptions() { + onNodeWithTag(TestTag.Prescriptions.Content) + .performScrollToIndex(0) + .performTouchInput { + swipeDown() + } + composeRule.sleep(2500L) + } + + fun userSeesPZNPrescription( + data: Prescription, + inState: PrescriptionState = PrescriptionState.Redeemable + ) { + val prescriptionNode = onNodeWithTag(TestTag.Prescriptions.Content, useUnmergedTree = true) + .assertIsDisplayed() + .performScrollToKey("prescription-${data.taskId}") + .onChildren() + .filterToOne(hasPrescriptionId(data.taskId)) + .onChildren() + + prescriptionNode + .filterToOne(hasTestTag(TestTag.Prescriptions.FullDetailPrescriptionName)) + .assertTextContains(data.medication?.name ?: "") + + val expectedTestTag = when (inState) { + PrescriptionState.Redeemable -> TestTag.Prescriptions.PrescriptionRedeemable + PrescriptionState.WaitForResponse -> TestTag.Prescriptions.PrescriptionWaitForResponse + PrescriptionState.InProgress -> TestTag.Prescriptions.PrescriptionInProgress + PrescriptionState.Redeemed -> TestTag.Prescriptions.PrescriptionRedeemed + } + prescriptionNode + .filterToOne(hasTestTag(expectedTestTag)) + .assertIsDisplayed() + } + + fun userSeesPrescriptionInArchive( + data: Prescription, + inState: PrescriptionState = PrescriptionState.Redeemable + ) { + val prescriptionNode = onNodeWithTag(TestTag.Prescriptions.Archive.Content, useUnmergedTree = true) + .assertIsDisplayed() + .performScrollToKey("prescription-${data.taskId}") + .onChildren() + .filterToOne(hasPrescriptionId(data.taskId)) + .onChildren() + + prescriptionNode + .filterToOne(hasTestTag(TestTag.Prescriptions.FullDetailPrescriptionName)) + .assertTextContains(data.medication?.name ?: "") + + val expectedTestTag = when (inState) { + PrescriptionState.Redeemable -> TestTag.Prescriptions.PrescriptionRedeemable + PrescriptionState.WaitForResponse -> TestTag.Prescriptions.PrescriptionWaitForResponse + PrescriptionState.InProgress -> TestTag.Prescriptions.PrescriptionInProgress + PrescriptionState.Redeemed -> TestTag.Prescriptions.PrescriptionRedeemed + } + prescriptionNode + .filterToOne(hasTestTag(expectedTestTag)) + .assertIsDisplayed() + } + + fun clickOnPrescription(data: Prescription) { + onNodeWithTag(TestTag.Prescriptions.Content) + .assertIsDisplayed() + .performScrollToKey("prescription-${data.taskId}") + .onChildren() + .filterToOne(hasPrescriptionId(data.taskId)) + .performClick() + } + + fun userSeesPrescriptionDetails() { + composeRule.await(TestConfig.ScreenChangeTimeout) { + onNodeWithTag(TestTag.Prescriptions.Details.Screen) + .assertIsDisplayed() + } + } + + fun userClicksMoreButton() { + onNodeWithTag(TestTag.Prescriptions.Details.MoreButton) + .assertIsDisplayed() + .performClick() + } + + fun userClicksDeleteButton() { + onNodeWithTag(TestTag.Prescriptions.Details.DeleteButton) + .assertIsDisplayed() + .performClick() + } + + fun userSeesConfirmDeleteDialog() { + onNodeWithTag(TestTag.AlertDialog.Modal, useUnmergedTree = true) + .assertIsDisplayed() + } + + fun userConfirmsDeletion() { + onNodeWithTag(TestTag.AlertDialog.ConfirmButton) + .assertIsDisplayed() + .performClick() + } + + fun userMissesPrescription(data: Prescription) { + onNodeWithTag(TestTag.Prescriptions.Content) + .assertIsDisplayed() + .onChildren() + .filter(hasPrescriptionId(data.taskId)) + .assertCountEquals(0) + } + + fun userClicksArchiveButton() { + onNodeWithTag(TestTag.Prescriptions.Content) + .performScrollToNode(hasTestTag(TestTag.Prescriptions.ArchiveButton)) + + onNodeWithTag(TestTag.Prescriptions.ArchiveButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userSeesPrescriptionSortedBy(prescriptions: List) { + val node = onNodeWithTag(TestTag.Prescriptions.Content) + .fetchSemanticsNode() + val config = node.config[SemanticsProperties.IndexForKey] + + val all = prescriptions.map { prescription -> + val index = config.invoke("prescription-${prescription.taskId}") + "$index - ${prescription.taskId} - ${prescription.authoredOn}" + }.joinToString("\n") + + prescriptions.fold(-1) { previousIndex, prescription -> + val index = config.invoke("prescription-${prescription.taskId}") + val msg = "Index should match: $index > $previousIndex for ${prescription.taskId}\n$all" + assertTrue(msg, index > previousIndex) + index + } + } + + // + + private fun onDetailsNode(testTag: String, contentTestTag: String) = + onNodeWithTag(contentTestTag) + .assertIsDisplayed() + .performScrollToNode(hasTestTag(testTag)) + .onChildren() + .filterToOne(hasTestTag(testTag)) + + private fun assertWith( + testTag: String, + contentTestTag: String, + with: SemanticsNodeInteraction.() -> Unit + ) { + onDetailsNode(testTag, contentTestTag).with() + } + + private fun assertText(testTag: String, contentTestTag: String, vararg data: String?) { + val node = onDetailsNode(testTag, contentTestTag).assertHasText(includeEditableText = false) + val dataFiltered = data.filterNotNull() + Log.d("assertText", "Assert hasText: ${dataFiltered.joinToString(" and ") { "[$it]" }} on $testTag") + + dataFiltered.forEach { + node + .assertTextContains(it, substring = true, ignoreCase = true) + } + } + + private fun assertDetailsText(testTag: String, vararg data: String?) = + assertText(testTag = testTag, contentTestTag = TestTag.Prescriptions.Details.Content, *data) + + fun userExpectsPrescriptionData(data: Prescription) { + assertDetailsText(TestTag.Prescriptions.Details.MedicationButton, data.medication?.name) + assertDetailsText(TestTag.Prescriptions.Details.PatientButton, data.patient?.firstName, data.patient?.lastName) + assertDetailsText(TestTag.Prescriptions.Details.PrescriberButton, data.practitioner?.name) + } + + // technical details + + fun userClicksOnTechnicalDetails() { + onDetailsNode(TestTag.Prescriptions.Details.TechnicalInformationButton, TestTag.Prescriptions.Details.Content) + .assertHasClickAction() + .performClick() + } + + fun userSeesTechnicalDetailsScreen() { + onNodeWithTag(TestTag.Prescriptions.Details.TechnicalInformation.Screen) + .assertIsDisplayed() + } + + private fun assertTechnicalText(testTag: String, vararg data: String?) = + assertText( + testTag = testTag, + contentTestTag = TestTag.Prescriptions.Details.TechnicalInformation.Content, + *data + ) + + fun userExpectsTechnicalInformationData(data: Prescription) { + assertTechnicalText(TestTag.Prescriptions.Details.TechnicalInformation.TaskId, data.taskId) + assertTechnicalText(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode, data.accessCode) + } + + // patient + + fun userClicksOnPatientDetails() { + onDetailsNode(TestTag.Prescriptions.Details.PatientButton, TestTag.Prescriptions.Details.Content) + .assertHasClickAction() + .performClick() + } + + fun userSeesPatientDetailsScreen() { + onNodeWithTag(TestTag.Prescriptions.Details.Patient.Screen) + .assertIsDisplayed() + } + + private fun assertPatientText(testTag: String, vararg data: String?) = + assertText( + testTag = testTag, + contentTestTag = TestTag.Prescriptions.Details.Patient.Content, + *data + ) + + fun userExpectsPatientDetailsData(data: Prescription) { + assertPatientText(TestTag.Prescriptions.Details.Patient.KVNR, data.patient?.kvnr) + assertPatientText(TestTag.Prescriptions.Details.Patient.BirthDate, data.patient?.birthDate) + assertPatientText(TestTag.Prescriptions.Details.Patient.Name, data.patient?.firstName, data.patient?.lastName) + assertPatientText(TestTag.Prescriptions.Details.Patient.InsuranceName, data.coverage?.insuranceName) + assertPatientText( + TestTag.Prescriptions.Details.Patient.Address, + data.patient?.city, + data.patient?.postal, + data.patient?.street + ) + assertWith( + testTag = TestTag.Prescriptions.Details.Patient.InsuranceState, + contentTestTag = TestTag.Prescriptions.Details.Patient.Content, + with = { + data.coverage?.insuranceState?.let { + assert(hasInsuranceState(it)) + } + } + ) + } + + // organization + + fun userClicksOnOrganizationDetails() { + onDetailsNode(TestTag.Prescriptions.Details.OrganizationButton, TestTag.Prescriptions.Details.Content) + .assertHasClickAction() + .performClick() + } + + fun userSeesOrganizationDetailsScreen() { + onNodeWithTag(TestTag.Prescriptions.Details.Organization.Screen) + .assertIsDisplayed() + } + + private fun assertOrganizationText(testTag: String, vararg data: String?) = + assertText( + testTag = testTag, + contentTestTag = TestTag.Prescriptions.Details.Organization.Content, + *data + ) + + fun userExpectsOrganizationDetailsData(data: Prescription) { + assertOrganizationText(TestTag.Prescriptions.Details.Organization.Name, data.practitioner?.officeName) + assertOrganizationText( + TestTag.Prescriptions.Details.Organization.Address, + data.practitioner?.city, + data.practitioner?.postal, + data.practitioner?.street + ) + assertOrganizationText(TestTag.Prescriptions.Details.Organization.BSNR, data.practitioner?.bsnr) + assertOrganizationText(TestTag.Prescriptions.Details.Organization.Phone, data.practitioner?.phone) + assertOrganizationText(TestTag.Prescriptions.Details.Organization.EMail, data.practitioner?.email) + } + + // medication + + fun userClicksOnMedicationDetails() { + onDetailsNode(TestTag.Prescriptions.Details.MedicationButton, TestTag.Prescriptions.Details.Content) + .assertHasClickAction() + .performClick() + } + + fun userSeesMedicationDetailsScreen() { + onNodeWithTag(TestTag.Prescriptions.Details.Medication.Screen) + .assertIsDisplayed() + } + + private fun assertMedicationText(testTag: String, vararg data: String?) = + assertText( + testTag = testTag, + contentTestTag = TestTag.Prescriptions.Details.Medication.Content, + *data + ) + + fun userExpectsMedicationDetailsData(data: Prescription) { + assertMedicationText(TestTag.Prescriptions.Details.Medication.Name, data.medication?.name) + assertMedicationText(TestTag.Prescriptions.Details.Medication.Amount, data.medication?.amount.toString()) + assertMedicationText(TestTag.Prescriptions.Details.Medication.PZN, data.medication?.pzn.toString()) + assertMedicationText(TestTag.Prescriptions.Details.Medication.DosageInstruction, data.medication?.dosage) + assertWith( + testTag = TestTag.Prescriptions.Details.Medication.SubstitutionAllowed, + contentTestTag = TestTag.Prescriptions.Details.Medication.Content, + with = { + data.medication?.substitutionAllowed?.let { + assert(hasSubstitutionAllowed(it)) + } + } + ) + assertWith( + testTag = TestTag.Prescriptions.Details.Medication.SupplyForm, + contentTestTag = TestTag.Prescriptions.Details.Medication.Content, + with = { + data.medication?.supplyForm?.let { + assert(hasSupplyForm(it)) + } + } + ) + assertWith( + testTag = TestTag.Prescriptions.Details.Medication.Category, + contentTestTag = TestTag.Prescriptions.Details.Medication.Content, + with = { + data.medication?.category?.let { + assert(hasMedicationCategory(it)) + } + } + ) + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt new file mode 100644 index 00000000..84eea35d --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileSettingsScreen.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextReplacement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class ProfileSettingsScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesProfileSettingsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.ProfileScreen) + } + fun userSeesDeleteProfileAlert(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.AlertDialog.Modal) + } + + fun tapDisplayTokensButton() { + onNodeWithTag(TestTag.Profile.ProfileScreenContent) + .performScrollToNode(hasTestTag(TestTag.Profile.OpenTokensScreenButton)) + onNodeWithTag(TestTag.Profile.OpenTokensScreenButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun checkKVNRNotVisible() { + onNodeWithTag(TestTag.Profile.InsuranceId) + .assertDoesNotExist() + } + + fun checkKVNRIsVisible() { + onNodeWithTag(TestTag.Profile.InsuranceId) + .assertIsDisplayed() + .assert(!hasText("")) + } + + fun tapLoginButton() { + onNodeWithTag(TestTag.Profile.LoginButton) + .assertIsDisplayed() + .performClick() + } + + fun assertHintTextNotPresent() { + onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) + .assertDoesNotExist() + } + + fun tapThreeDotsMenuButton() { + onNodeWithTag(TestTag.Profile.ThreeDotMenuButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapAuditEventsButton() { + onNodeWithTag(TestTag.Profile.ProfileScreenContent) + .performScrollToNode(hasTestTag(TestTag.Profile.OpenAuditEventsScreenButton)) + onNodeWithTag(TestTag.Profile.OpenAuditEventsScreenButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapLogoutButton() { + onNodeWithTag(TestTag.Profile.LogoutButton) + .assertIsDisplayed() + .performClick() + } + + fun closeProfileSettings() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun tapDeleteProfile() { + onNodeWithTag(TestTag.Profile.DeleteProfileButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapConfirmDeleteProfile() { + onNodeWithTag(TestTag.AlertDialog.ConfirmButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapAbortDeleteButton() { + onNodeWithTag(TestTag.AlertDialog.CancelButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapEditProfileNameButton() { + onNodeWithTag(TestTag.Profile.EditProfileNameButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun enterNewProfileName(newProfileName: String) { + onNodeWithTag(TestTag.Profile.NewProfileNameField) + .assertIsDisplayed() + .performClick() + .performTextReplacement(newProfileName) + } + + fun assertErrorMessageEmptyProfileName(errorMessage: String) { + onNodeWithText(errorMessage) + .assertIsDisplayed() + } + + fun tapEditProfileIconButton() { + onNodeWithTag(TestTag.Profile.EditProfileImageButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun changeProfilePictureColor(color: String) { + val colorToTestTag = mapOf( + "grau" to TestTag.Profile.EditProfileIcon.ColorSelectorSpringGrayButton, + "gelb" to TestTag.Profile.EditProfileIcon.ColorSelectorSunDewButton, + "rosa" to TestTag.Profile.EditProfileIcon.ColorSelectorPinkButton, + "grün" to TestTag.Profile.EditProfileIcon.ColorSelectorTreeButton, + "blau" to TestTag.Profile.EditProfileIcon.ColorSelectorBlueMoonButton + ) + + val testTag = colorToTestTag[color.lowercase()] + + testTag?.let { + onNodeWithTag(testTag) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + } + + fun tapBackToSettingsButton() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt new file mode 100644 index 00000000..29303154 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/ProfileTokenListScreen.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.assertHasText +import de.gematik.ti.erp.app.test.test.core.awaitDisplay + +class ProfileTokenListScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesProfileTokenListScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Profile.TokenList.TokenScreen) + } + + fun checkTokensDoNotExist() { + onNodeWithTag(TestTag.Profile.TokenList.AccessToken) + .assertDoesNotExist() + onNodeWithTag(TestTag.Profile.TokenList.SSOToken) + .assertDoesNotExist() + } + + fun checkAccessTokenPresent() { + onNodeWithTag(TestTag.Profile.TokenList.AccessToken) + .assertIsDisplayed() + .assertHasText() + } + + fun checkSSOTokenPresent() { + onNodeWithTag(TestTag.Profile.TokenList.SSOToken) + .assertIsDisplayed() + .assertHasText() + } + + fun assertHeaderTextNotEmpty() { + onNodeWithTag(TestTag.Profile.TokenList.NoTokenHeader) + .assertIsDisplayed() + .assertHasText() + } + + fun assertInfoTextNotEmpty() { + onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) + .assertIsDisplayed() + .assertHasText() + } + + fun assertInfoTextPresent(text: String) { + onNodeWithTag(TestTag.Profile.TokenList.NoTokenInfo) + .assertIsDisplayed() + .assertTextContains(text, substring = true) + } + + fun closeTokenList() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt new file mode 100644 index 00000000..5dce5557 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/screens/SettingsScreen.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay +import junit.framework.TestCase.assertTrue + +class SettingsScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesSettingsScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.SettingsScreen) + } + fun userSeesCreateNewProfileAlert(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Settings.AddProfileDialog.Modal) + } + + private val profileSettingsScreen by lazy { ProfileSettingsScreen(composeRule) } + + fun goToFirstProfileDetails() { + goToProfileDetails(0) + } + + fun goToProfileDetails(index: Int) { + tapProfileDetailsButton(index) + profileSettingsScreen.userSeesProfileSettingsScreen() + } + + fun assertAmountOfProfiles(numberOfProfiles: Int) { + assertTrue( + onAllNodesWithTag(TestTag.Settings.ProfileButton) + .fetchSemanticsNodes().size == numberOfProfiles + ) + } + + fun tapDebugMenuButton() { + onNodeWithTag(TestTag.Settings.DebugMenuButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun enterProfileName(name: String) { + onNodeWithTag(TestTag.Settings.AddProfileDialog.ProfileNameTextField) + .assertIsDisplayed() + .performClick() + .performTextInput(name) + } + + fun tapNewProfileConfirmButton() { + onNodeWithTag(TestTag.Settings.AddProfileDialog.ConfirmButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapReturnToPrescriptionScreen() { + onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapOrderEgk() { + onNodeWithTag(TestTag.Settings.OrderNewCardButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapProfileWithName(profileName: String) { + onNodeWithText(profileName) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapProfileDetailsButton(profileNumber: Int) { + onAllNodesWithTag(TestTag.Settings.ProfileButton)[profileNumber] + .assertIsDisplayed() + .performClick() + } + + fun checkAmountOfProfilesWithNames(numberOfProfiles: Int, profileNames: String) { + assert(onAllNodesWithText(profileNames).fetchSemanticsNodes().size == numberOfProfiles) + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt new file mode 100644 index 00000000..0c910023 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/CardWallScreenSteps.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.steps + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.test.platform.app.InstrumentationRegistry +import de.gematik.ti.erp.app.test.test.VirtualEgk1 +import de.gematik.ti.erp.app.test.test.VirtualEgkWithPrescription +import de.gematik.ti.erp.app.test.test.screens.CardWallScreen +import de.gematik.ti.erp.app.test.test.screens.DebugMenuScreen +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.SettingsScreen + +class CardWallScreenSteps(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + private val mainScreen by lazy { MainScreen(composeRule) } + private val cardWallScreen by lazy { CardWallScreen(composeRule) } + private val settingsScreen by lazy { SettingsScreen(composeRule) } + private val debugMenuScreen by lazy { DebugMenuScreen(composeRule) } + + fun userStartsAndFinishsTheCardwallSuccessfully() { + InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc enable") + + mainScreen.userSeesMainScreen() + mainScreen.refreshMainScreenBySwipe() + mainScreen.tapLoginButton() + + cardWallScreen.continueWithEGK() + cardWallScreen.enterCAN() + cardWallScreen.enterPin() + cardWallScreen.dontSaveCredentials() + cardWallScreen.userSeesNfcScreen() + + InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc disable") + Thread.sleep(2000) + InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("svc nfc enable") + + mainScreen.userSeesMainScreen(15000) + } + + fun userStartsAndFinishsTheCardwallWithVirtualCardSuccessfully() { + mainScreen.userSeesMainScreen() + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapDebugMenuButton() + debugMenuScreen.userSeesDebugMenuScreen() + debugMenuScreen.tapSetVirtualCard() + debugMenuScreen.closeDebugMenu() + settingsScreen.tapReturnToPrescriptionScreen() + mainScreen.userSeesMainScreen() + } + + fun fakeNFCCapabilities() { + mainScreen.userSeesMainScreen() + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapDebugMenuButton() + debugMenuScreen.userSeesDebugMenuScreen() + debugMenuScreen.tapFakeNFCCapabilitiesSwitch() + debugMenuScreen.closeDebugMenu() + settingsScreen.tapReturnToPrescriptionScreen() + mainScreen.userSeesMainScreen() + } + + fun setCustomEgk() { + // only for demonstration purposes + mainScreen.userSeesMainScreen() + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapDebugMenuButton() + debugMenuScreen.userSeesDebugMenuScreen() + debugMenuScreen.fillCustomCertificateAndPrivateKey(VirtualEgk1) + debugMenuScreen.tapSetVirtualCard() + debugMenuScreen.closeDebugMenu() + settingsScreen.tapReturnToPrescriptionScreen() + mainScreen.userSeesMainScreen() + } + + fun setVirtualEGKWithPrescriptions() { + // only for demonstration purposes + mainScreen.userSeesMainScreen() + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapDebugMenuButton() + debugMenuScreen.userSeesDebugMenuScreen() + debugMenuScreen.fillCustomCertificateAndPrivateKey(VirtualEgkWithPrescription) + debugMenuScreen.tapSetVirtualCard() + debugMenuScreen.closeDebugMenu() + settingsScreen.tapReturnToPrescriptionScreen() + mainScreen.userSeesMainScreen() + } + + fun userClicksOrderHealthCardFromCardWallIntroScreen() { + cardWallScreen.userSeesIntroScreen() + cardWallScreen.tapOrderEgkFromIntroScreen() + } + + fun userClicksOrderHealthCardFromCardWallCANScreen() { + cardWallScreen.userSeesIntroScreen() + cardWallScreen.continueWithEGK() + cardWallScreen.userSeesCANScreen() + cardWallScreen.tapOrderEgkFromCANScreen() + } + + fun userClicksOrderHealthCardFromCardWallPinScreen() { + cardWallScreen.userSeesIntroScreen() + cardWallScreen.continueWithEGK() + cardWallScreen.userSeesCANScreen() + cardWallScreen.enterCAN() + cardWallScreen.userSeesPinScreen() + cardWallScreen.tapOrderEgkFromPinScreen() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt new file mode 100644 index 00000000..cad1658c --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/MainScreenSteps.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.steps + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import de.gematik.ti.erp.app.test.test.screens.MainScreen + +class MainScreenSteps(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + private val mainScreen by lazy { MainScreen(composeRule) } + + fun userTapsSettingsMenuButton() { + mainScreen.tapSettingsButton() + } + + fun createNewProfile(profileName: String) { + userTapsAddProfileButton() + mainScreen.enterProfileName(profileName) + if ("" != profileName) { + mainScreen.tapNewProfileConfirmButton() + mainScreen.userSeesMainScreen() + } + } + + fun userTapsAddProfileButton() { + mainScreen.tapAddProfileButton() + mainScreen.userSeesBottomSheet() + } + + fun userTapsAbort() { + mainScreen.userSeesBottomSheet() + mainScreen.tapCancelAddProfileButton() + mainScreen.userClicksBottomBarPrescriptions() + } + + fun userCantConfirmCreation() { + mainScreen.userSeesBottomSheet() + mainScreen.assertConfirmationCanNotBeClicked() + } + + fun userTapsConnect() { + mainScreen.tapLoginButton() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt new file mode 100644 index 00000000..fabb05a8 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/OnboardingSteps.kt @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.steps + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.sleep +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.OnboardingScreen + +class OnboardingSteps( + private val composeRule: ComposeTestRule +) : SemanticsNodeInteractionsProvider by composeRule { + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + + enum class Page { + DataTerms, Credentials, Analytics, MainScreen + } + + fun userSeesMainScreen() { + mainScreen.userSeesMainScreen() + } + + fun userIsNotSeeingTheOnboarding() { + onboardingScreen.checkTutorialIsNotPresent() + } + + fun userIsFinishingTheOnboardingWithoutAnalytics() { + userNavigatesToOnboardingScreenName(Page.MainScreen) + } + + fun userIsFinishingTheOnboardingWithAnalytics() { + userNavigatesToOnboardingScreenName(Page.Analytics) + onboardingScreen.toggleAnalyticsSwitch() + onboardingScreen.waitForAnalyticsPage() + onboardingScreen.tapAcceptAnalyticsButton() + onboardingScreen.checkAnalyticsSwitchIsActivated() + onboardingScreen.checkContinueTutorialButtonIsEnabled() + onboardingScreen.tapContinueButton() + mainScreen.userSeesMainScreen() + } + + fun userSkipsOnboarding() { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.tapConnectLater() + mainScreen.userSeesMainScreen() + tapToGetRidOfTour() + } + + fun tapToGetRidOfTour() { + mainScreen.userClicksBottomBarPrescriptions() + mainScreen.userClicksBottomBarPrescriptions() + mainScreen.userClicksBottomBarPrescriptions() + mainScreen.userClicksBottomBarPrescriptions() + mainScreen.userClicksBottomBarPrescriptions() + composeRule.sleep(2000L) + } + + fun userSeesWelcomeScreen() { + onboardingScreen.checkWelcomePageIsPresent() + } + + fun userNavigatesToOnboardingScreenName(page: Page) { + when (page) { + // go to Data Terms Screen + Page.DataTerms -> { + onboardingScreen.waitForSecondOnboardingPage() + onboardingScreen.checkDataTermsPageIsPresent() + } + Page.Credentials -> { // go to Password Screen + onboardingScreen.waitForSecondOnboardingPage() + onboardingScreen.tapDataTermsSwitch() + onboardingScreen.tapContinueButton() + onboardingScreen.checkCredentialsPageIsPresent() + } + Page.Analytics -> { // go to Analytics Screen + userNavigatesToOnboardingScreenName(Page.Credentials) + onboardingScreen.switchToPasswordMode() + userEntersStrongEnoughPasswordTwice() + onboardingScreen.tapContinueButton() + userSeesAnalyticsScreen() + } + Page.MainScreen -> { + userNavigatesToOnboardingScreenName(Page.Analytics) + onboardingScreen.tapContinueButton() + mainScreen.userSeesMainScreen() + } + } + } + + private fun userSeesAnalyticsScreen() { + onboardingScreen.checkAnalyticsPageIsPresent() + onboardingScreen.checkContinueTutorialButtonIsEnabled() + onboardingScreen.checkAnalyticsSwitchIsDeactivated() + } + + fun userSeesCredentialScreen() { + onboardingScreen.checkCredentialsPageIsPresent() + } + + fun dataTermsSwitchDeactivated() { + onboardingScreen.checkDataTermsSwitchDeactivated() + } + + fun confirmContinueButtonIsDeactivated() { + onboardingScreen.checkContinueTutorialButtonIsDeactivated() + } + + fun toggleDataTermsSwitch() { + onboardingScreen.tapDataTermsSwitch() + } + + fun userEntersAWeakPasswordTwice() { + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.WeakPassword) + onboardingScreen.enterPasswordB(TestConfig.WeakPassword) + } + + fun userEntersStrongEnoughPasswordTwice() { + onboardingScreen.switchToPasswordMode() + onboardingScreen.enterPasswordA(TestConfig.StrongPassword) + onboardingScreen.enterPasswordB(TestConfig.StrongPassword) + onboardingScreen.checkNoPasswordErrorMessagePresent() + } + + fun userSwitchesToPasswordMode() { + onboardingScreen.switchToPasswordMode() + } + + fun userDoesNotSeeContinueButton() { + onboardingScreen.checkContinueTutorialButtonIsDisabled() + } + + fun userSeesActivatedContinueButton() { + onboardingScreen.checkContinueTutorialButtonIsEnabled() + } + + fun userSeesTermsOfUse() { + onboardingScreen.checkTermsOfUseAreDisplayed() + } + + fun userSeesNoTermsOfUse() { + onboardingScreen.checkTermsOfUseAreNotDisplayed() + } + + fun userOpensTermsOfUse() { + onboardingScreen.openTermsOfUse() + } + + fun userClosesTermsOfUse() { + onboardingScreen.closeTermsOfUse() + } + + fun userDoesntSeeDataProtection() { + onboardingScreen.checkDataProtectionAreNotDisplayed() + } + + fun userOpensDataProtection() { + onboardingScreen.openDataProtection() + } + + fun userSeesDataProtection() { + onboardingScreen.checkDataProtectionIsDisplayed() + } + + fun userClosesDataProtection() { + onboardingScreen.closeDataProtection() + } + + fun userSeesErrorMessageForPasswordStrength() { + onboardingScreen.checkPasswordErrorMessagePresent() + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt new file mode 100644 index 00000000..da59de5f --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/ProfileSettingsSteps.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.steps + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import de.gematik.ti.erp.app.test.test.screens.AuditEventsScreen +import de.gematik.ti.erp.app.test.test.screens.CardWallScreen +import de.gematik.ti.erp.app.test.test.screens.MainScreen +import de.gematik.ti.erp.app.test.test.screens.ProfileSettingsScreen +import de.gematik.ti.erp.app.test.test.screens.ProfileTokenListScreen +import de.gematik.ti.erp.app.test.test.screens.SettingsScreen + +class ProfileSettingsSteps( + private val composeRule: ComposeTestRule +) : SemanticsNodeInteractionsProvider by composeRule { + + private val mainScreen by lazy { MainScreen(composeRule) } + private val profileSettingsScreen by lazy { ProfileSettingsScreen(composeRule) } + private val profileTokenListScreen by lazy { ProfileTokenListScreen(composeRule) } + private val cardWallScreen by lazy { CardWallScreen(composeRule) } + private val settingsScreen by lazy { SettingsScreen(composeRule) } + private val auditEventsScreen by lazy { AuditEventsScreen(composeRule) } + + fun openProfileSettings() { + mainScreen.tapSettingsButton() + settingsScreen.goToFirstProfileDetails() + profileSettingsScreen.userSeesProfileSettingsScreen() + } + + fun openProfileSettingsForCertainProfile(profileIndex: Int) { + mainScreen.userSeesMainScreen() + // to keep scenarios human-readable, we'll hide the fact, that the index actually starts at zero ¯\_(ツ)_/¯ + mainScreen.tapSettingsButton() + settingsScreen.goToProfileDetails(profileIndex - 1) + } + + fun checkNoTokenPresent() { + profileSettingsScreen.tapDisplayTokensButton() + profileTokenListScreen.userSeesProfileTokenListScreen() + profileTokenListScreen.checkTokensDoNotExist() + } + + fun checkTokenHintPresent() { + profileTokenListScreen.assertHeaderTextNotEmpty() + profileTokenListScreen.assertInfoTextNotEmpty() + } + + fun checkTokenHintPresent(text: String) { + profileSettingsScreen.tapDisplayTokensButton() + profileTokenListScreen.assertInfoTextPresent(text) + } + + fun checkNoKVNRIsVisible() { + profileSettingsScreen.checkKVNRNotVisible() + } + + fun checkKVNRIsVisible() { + profileSettingsScreen.checkKVNRIsVisible() + } + + fun tapLoginButton() { + profileSettingsScreen.tapThreeDotsMenuButton() + profileSettingsScreen.tapLoginButton() + } + + fun userSeesCardwallWelcomeScreen() { + cardWallScreen.userSeesIntroScreen() + } + + fun checkTokenPresent() { + profileSettingsScreen.tapDisplayTokensButton() + profileTokenListScreen.userSeesProfileTokenListScreen() + profileTokenListScreen.checkAccessTokenPresent() + profileTokenListScreen.checkSSOTokenPresent() + profileTokenListScreen.closeTokenList() + } + + fun hintTextNotPresent() { + profileSettingsScreen.tapDisplayTokensButton() + profileSettingsScreen.assertHintTextNotPresent() + } + + fun logoutViaProfileSettings() { + mainScreen.tapSettingsButton() + settingsScreen.goToFirstProfileDetails() + profileSettingsScreen.tapThreeDotsMenuButton() + profileSettingsScreen.tapLogoutButton() + profileSettingsScreen.closeProfileSettings() + } + + fun noTokenPresentInProfileSettings() { + mainScreen.tapSettingsButton() + settingsScreen.goToFirstProfileDetails() + profileSettingsScreen.tapDisplayTokensButton() + profileTokenListScreen.checkTokensDoNotExist() + profileTokenListScreen.closeTokenList() + } + + fun userSeesAuditEventsScreen(profileIndex: Int) { + mainScreen.userSeesMainScreen() + mainScreen.tapSettingsButton() + // -1 durch Differenzen zum normalen Sprachgebrauch + settingsScreen.goToProfileDetails(profileIndex - 1) + profileSettingsScreen.tapAuditEventsButton() + auditEventsScreen.userSeesAuditEventsScreen() + } + + fun userSeesEmptyStateForCertainProfile() { + auditEventsScreen.userSeesAuditEventsScreen() + auditEventsScreen.checkAuditEventsDoNotExist() + } + + fun userLogsOutOffProfile(index: Int) { + mainScreen.tapSettingsButton() + settingsScreen.goToProfileDetails(index - 1) + profileSettingsScreen.tapThreeDotsMenuButton() + profileSettingsScreen.tapLogoutButton() + profileSettingsScreen.closeProfileSettings() + mainScreen.userSeesMainScreen() + } + + fun userDoesNotSeesEmptyStateForCertainProfile() { + auditEventsScreen.userSeesAuditEventsScreen() + auditEventsScreen.checkNoAuditEventsHeaderAndInfoDoesNotExist() + } + + fun userHasNumberOfProfiles(numberOfProfiles: Int) { + mainScreen.tapSettingsButton() + settingsScreen.assertAmountOfProfiles(numberOfProfiles) + mainScreen.userClicksBottomBarPrescriptions() + mainScreen.userSeesMainScreen() + } + + fun userDeletesProfile(profileName: String) { + mainScreen.tapSettingsButton() + settingsScreen.tapProfileWithName(profileName) + profileSettingsScreen.userSeesProfileSettingsScreen() + profileSettingsScreen.tapThreeDotsMenuButton() + profileSettingsScreen.tapDeleteProfile() + profileSettingsScreen.tapConfirmDeleteProfile() + } + + fun userHasProfilesWithName(numberOfProfiles: Int, profileNames: String) { + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.checkAmountOfProfilesWithNames(numberOfProfiles, profileNames) + } + + fun createProfileAfterLastOneWasDeleted() { + settingsScreen.userSeesCreateNewProfileAlert() + settingsScreen.enterProfileName("Profil 1") + settingsScreen.tapNewProfileConfirmButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapReturnToPrescriptionScreen() + mainScreen.userSeesMainScreen() + } + + fun userInterruptsDeletingProfile(profileName: String) { + navigateToProfileWithName(profileName) + profileSettingsScreen.userSeesProfileSettingsScreen() + profileSettingsScreen.tapThreeDotsMenuButton() + profileSettingsScreen.tapDeleteProfile() + profileSettingsScreen.tapAbortDeleteButton() + profileSettingsScreen.tapBackToSettingsButton() + } + + fun editProfileName(profileName: String, newProfileName: String) { + navigateToProfileWithName(profileName) + profileSettingsScreen.tapEditProfileNameButton() + profileSettingsScreen.enterNewProfileName(newProfileName) + } + + fun assertErrorMessageEmptyProfileName(errorMessage: String) { + profileSettingsScreen.assertErrorMessageEmptyProfileName(errorMessage) + } + + fun userChangesProfilePictureOfProfileFromColorTo(profileName: String, color: String) { + navigateToProfileWithName(profileName) + profileSettingsScreen.tapEditProfileIconButton() + profileSettingsScreen.changeProfilePictureColor(color) + profileSettingsScreen.tapBackToSettingsButton() + } + + private fun navigateToProfileWithName(profileName: String) { + mainScreen.tapSettingsButton() + settingsScreen.userSeesSettingsScreen() + settingsScreen.tapProfileWithName(profileName) + } +} diff --git a/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt new file mode 100644 index 00000000..a9c05ed4 --- /dev/null +++ b/app/android/src/androidTest/java/de/gematik/ti/erp/app/test/test/steps/SettingScreenSteps.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.test.test.steps + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.test.test.TestConfig +import de.gematik.ti.erp.app.test.test.core.awaitDisplay +import de.gematik.ti.erp.app.test.test.screens.OrderEgkScreen +import de.gematik.ti.erp.app.test.test.screens.SettingsScreen + +class SettingScreenSteps(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + private val settingsScreen by lazy { SettingsScreen(composeRule) } + private val orderEgkScreen by lazy { OrderEgkScreen(composeRule) } + + fun userWantsToOrderNewCard() { + settingsScreen.tapOrderEgk() + orderEgkScreen.userSeesOrderEgkScreenScreen() + } + + fun userUsesLinkAboutNFC() { + orderEgkScreen.tapNFCExplanationPageLink() + } + + fun userIsNotInERezeptAppAnymore() { + // TODO Wir sind im browser und wollen den URL prüfen. URL = "das-e-rezept-fuer-deutschland.de" + } + + fun userSeesAListOfInsurances() { + orderEgkScreen.userSeesOrderEgkScreenScreen() + orderEgkScreen.checkIfAtLeastFourInsurerIsVisible() + } + + fun userChoosesInsurance(insurance: String) { + orderEgkScreen.chooseInsurance(insurance) + } + + fun userSeesOrderOptionScreen() { + composeRule.awaitDisplay(TestConfig.ScreenChangeTimeout, TestTag.Settings.OrderEgk.SelectOrderOptionScreen) + } + + fun userSeesHealthCardOrderContactScreen() { + composeRule.awaitDisplay(TestConfig.ScreenChangeTimeout, TestTag.Settings.OrderEgk.HealthCardOrderContactScreen) + } + + fun userSeesPossibilitiesWhatCanBeOrdered(orderPossibility: String) { + orderEgkScreen.checkOrderPossibilities(orderPossibility) + } + + fun userSeesPossibilitiesHowCanBeOrdered(contactPossibility: String) { + orderEgkScreen.checkContactPossibilities(contactPossibility) + } + + fun userAbortsOrderingOfNewCard() { + orderEgkScreen.tapOrderCardAbort() + } + + fun userSeesSettingsScreen() { + settingsScreen.userSeesSettingsScreen() + } +} diff --git a/android/src/debug/AndroidManifest.xml b/app/android/src/debug/AndroidManifest.xml similarity index 100% rename from android/src/debug/AndroidManifest.xml rename to app/android/src/debug/AndroidManifest.xml diff --git a/app/android/src/main/AndroidManifest.xml b/app/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e7d7cf8e --- /dev/null +++ b/app/android/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/assets/data_terms.html b/app/android/src/main/assets/data_terms.html similarity index 100% rename from android/src/main/assets/data_terms.html rename to app/android/src/main/assets/data_terms.html diff --git a/android/src/main/assets/open_source_licenses.json b/app/android/src/main/assets/open_source_licenses.json similarity index 100% rename from android/src/main/assets/open_source_licenses.json rename to app/android/src/main/assets/open_source_licenses.json diff --git a/android/src/main/assets/terms_of_use.html b/app/android/src/main/assets/terms_of_use.html similarity index 100% rename from android/src/main/assets/terms_of_use.html rename to app/android/src/main/assets/terms_of_use.html diff --git a/android/src/main/ic_launcher-playstore.png b/app/android/src/main/ic_launcher-playstore.png similarity index 100% rename from android/src/main/ic_launcher-playstore.png rename to app/android/src/main/ic_launcher-playstore.png diff --git a/android/src/main/java/de/gematik/ti/erp/app/App.kt b/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt similarity index 77% rename from android/src/main/java/de/gematik/ti/erp/app/App.kt rename to app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt index cb4bbbc7..f21c6cdf 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/App.kt +++ b/app/android/src/main/java/de/gematik/ti/erp/app/DefaultErezeptApp.kt @@ -18,13 +18,11 @@ package de.gematik.ti.erp.app -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.di.appModules +import de.gematik.ti.erp.app.di.featureModule import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier @@ -34,12 +32,13 @@ import org.kodein.di.android.x.androidXModule import org.kodein.di.bindSingleton import org.kodein.di.instance -class App : Application(), DIAware { +class DefaultErezeptApp : ErezeptApp(), DIAware { override val di by DI.lazy { - import(androidXModule(this@App)) - importAll(allModules) - bindSingleton { AuthenticationUseCase(instance(), instance()) } + import(androidXModule(this@DefaultErezeptApp)) + importAll(appModules) + importAll(featureModule, allowOverride = true) + bindSingleton { AuthenticationUseCase(instance()) } bindSingleton { VisibleDebugTree() } } @@ -49,7 +48,6 @@ class App : Application(), DIAware { override fun onCreate() { super.onCreate() - appContext = this if (BuildKonfig.INTERNAL) { Napier.base(DebugAntilog()) Napier.base(visibleDebugTree) @@ -58,17 +56,7 @@ class App : Application(), DIAware { ProcessLifecycleOwner.get().lifecycle.apply { addObserver(authUseCase) } - PDFBoxResourceLoader.init(this) - Contentsquare.start(this) } - - companion object { - lateinit var appContext: Context - - val cache = AppScopedCache() - } } - -fun app(): Application = App.appContext as Application diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt b/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt new file mode 100644 index 00000000..3f653a7e --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/di/AppModules.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +import android.content.Context +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.analytics.usecase.AnalyticsUseCase +import de.gematik.ti.erp.app.featuretoggle.FeatureToggleManager +import de.gematik.ti.erp.app.info.di.buildConfigInformationModule +import de.gematik.ti.erp.app.pkv.fileProviderAuthorityModule +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +private const val PREFERENCES_FILE_NAME = "appPrefs" +private const val NETWORK_SECURE_PREFS_FILE_NAME = "networkingSecurePrefs" +private const val NETWORK_PREFS_FILE_NAME = "networkingPrefs" +private const val MASTER_KEY_ALIAS = "netWorkMasterKey" + +const val ApplicationPreferencesTag = "ApplicationPreferences" +const val NetworkPreferencesTag = "NetworkPreferences" +const val NetworkSecurePreferencesTag = "NetworkSecurePreferences" + +@Requirement( + "A_20184#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Bind EncryptedSharedPreferences." +) +val appModules = DI.Module("appModules") { + bindSingleton { object : DispatchProvider {} } + bindSingleton(ApplicationPreferencesTag) { + val context = instance() + context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE) + } + bindSingleton(NetworkPreferencesTag) { + val context = instance() + context.getSharedPreferences(NETWORK_PREFS_FILE_NAME, Context.MODE_PRIVATE) + } + bindSingleton(NetworkSecurePreferencesTag) { + val context = instance() + + EncryptedSharedPreferences.create( + context, + NETWORK_SECURE_PREFS_FILE_NAME, + MasterKey.Builder(context, MASTER_KEY_ALIAS) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + bindSingleton { EndpointHelper(networkPrefs = instance(NetworkPreferencesTag)) } + + bindSingleton { FeatureToggleManager(instance()) } + + bindProvider { AnalyticsUseCase(instance()) } + + bindSingleton { Analytics(instance(), instance(ApplicationPreferencesTag), instance()) } + + importAll( + buildConfigInformationModule, + fileProviderAuthorityModule + ) +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt new file mode 100644 index 00000000..99db0cbd --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/di/ModuleNames.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +object ModuleNames { + const val cardWallModule = "cardWallModule" + const val integrityModule = "integrityModule" + const val networkModule = "networkModule" + const val pharmacyModule = "pharmacyModule" + const val buildConfigInformationModule = "buildConfigInformationModule" +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt b/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt new file mode 100644 index 00000000..b81d41d3 --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/info/DefaultBuildConfigInformation.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.info + +import android.content.Context +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import de.gematik.ti.erp.app.BuildConfig +import de.gematik.ti.erp.app.cardwall.usecase.deviceHasNFC +import java.util.Locale + +class DefaultBuildConfigInformation : BuildConfigInformation { + override fun versionName(): String = BuildConfig.VERSION_NAME + override fun versionCode(): String = "${BuildConfig.VERSION_CODE}" + override fun model(): String = "${Build.MANUFACTURER} ${Build.MODEL} (${Build.PRODUCT})" + override fun language(): String = Locale.getDefault().displayName + + @Composable + override fun inDarkTheme(): String = if (isSystemInDarkTheme()) DARK_THEME_ON else DARK_THEME_OFF + override fun nfcInformation(context: Context): String = + if (context.deviceHasNFC()) NFC_AVAILABLE else NFC_NOT_AVAILABLE + + companion object { + private const val DARK_THEME_ON = "an" + private const val DARK_THEME_OFF = "aus" + private const val NFC_AVAILABLE = "vorhanden" + private const val NFC_NOT_AVAILABLE = "nicht vorhanden" + } +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt b/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt new file mode 100644 index 00000000..016c42b0 --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/info/di/BuildConfigInformationModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.info.di + +import de.gematik.ti.erp.app.di.ModuleNames +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.info.DefaultBuildConfigInformation +// import de.gematik.ti.erp.app.info.BuildConfigInformation +// import de.gematik.ti.erp.app.info.DefaultBuildConfigInformation +import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.instance + +val buildConfigInformationModule = DI.Module(ModuleNames.buildConfigInformationModule) { + bind() with instance(DefaultBuildConfigInformation()) +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt new file mode 100644 index 00000000..ae1739d9 --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/DefaultFileProviderAuthority.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv + +import de.gematik.ti.erp.app.BuildConfig + +class DefaultFileProviderAuthority : FileProviderAuthority { + override fun getFilePath(): String { + return "${BuildConfig.APPLICATION_ID}.fileprovider" + } +} diff --git a/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt new file mode 100644 index 00000000..deca5d8a --- /dev/null +++ b/app/android/src/main/java/de/gematik/ti/erp/app/pkv/FileProviderAuthorityModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv + +import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.instance + +val fileProviderAuthorityModule = DI.Module("fileProviderAuthorityModule") { + bind() with instance(DefaultFileProviderAuthority()) +} diff --git a/android/src/main/res/font/noto_sans_bold.ttf b/app/android/src/main/res/font/noto_sans_bold.ttf similarity index 100% rename from android/src/main/res/font/noto_sans_bold.ttf rename to app/android/src/main/res/font/noto_sans_bold.ttf diff --git a/android/src/main/res/font/noto_sans_medium.ttf b/app/android/src/main/res/font/noto_sans_medium.ttf similarity index 100% rename from android/src/main/res/font/noto_sans_medium.ttf rename to app/android/src/main/res/font/noto_sans_medium.ttf diff --git a/android/src/main/res/font/noto_sans_regular.ttf b/app/android/src/main/res/font/noto_sans_regular.ttf similarity index 100% rename from android/src/main/res/font/noto_sans_regular.ttf rename to app/android/src/main/res/font/noto_sans_regular.ttf diff --git a/android/src/main/res/font/noto_sans_semibold.ttf b/app/android/src/main/res/font/noto_sans_semibold.ttf similarity index 100% rename from android/src/main/res/font/noto_sans_semibold.ttf rename to app/android/src/main/res/font/noto_sans_semibold.ttf diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to app/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/android/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xhdpi/ic_launcher.png rename to app/android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/app/android/src/main/res/mipmap-xhdpi/ic_launcher_background.webp similarity index 100% rename from android/src/main/res/mipmap-xhdpi/ic_launcher_background.webp rename to app/android/src/main/res/mipmap-xhdpi/ic_launcher_background.webp diff --git a/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp rename to app/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/android/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to app/android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/app/android/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp similarity index 100% rename from android/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp rename to app/android/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp diff --git a/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp rename to app/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to app/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/app/android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp similarity index 100% rename from android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp rename to app/android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp diff --git a/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp similarity index 100% rename from android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp rename to app/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/android/src/main/res/raw/analytics_identifier.json b/app/android/src/main/res/raw/analytics_identifier.json similarity index 100% rename from android/src/main/res/raw/analytics_identifier.json rename to app/android/src/main/res/raw/analytics_identifier.json diff --git a/android/src/main/res/raw/animation_courier.webm b/app/android/src/main/res/raw/animation_courier.webm similarity index 100% rename from android/src/main/res/raw/animation_courier.webm rename to app/android/src/main/res/raw/animation_courier.webm diff --git a/android/src/main/res/raw/animation_local.webm b/app/android/src/main/res/raw/animation_local.webm similarity index 100% rename from android/src/main/res/raw/animation_local.webm rename to app/android/src/main/res/raw/animation_local.webm diff --git a/android/src/main/res/raw/animation_mail.webm b/app/android/src/main/res/raw/animation_mail.webm similarity index 100% rename from android/src/main/res/raw/animation_mail.webm rename to app/android/src/main/res/raw/animation_mail.webm diff --git a/android/src/main/res/raw/animation_pulse_lottie.json b/app/android/src/main/res/raw/animation_pulse_lottie.json similarity index 100% rename from android/src/main/res/raw/animation_pulse_lottie.json rename to app/android/src/main/res/raw/animation_pulse_lottie.json diff --git a/android/src/main/res/raw/device_lottie.json b/app/android/src/main/res/raw/device_lottie.json similarity index 100% rename from android/src/main/res/raw/device_lottie.json rename to app/android/src/main/res/raw/device_lottie.json diff --git a/app/android/src/main/res/raw/health_insurance_contacts.json b/app/android/src/main/res/raw/health_insurance_contacts.json new file mode 100644 index 00000000..4913e9f6 --- /dev/null +++ b/app/android/src/main/res/raw/health_insurance_contacts.json @@ -0,0 +1,1157 @@ +[ + { + "name": "AOK Baden-Württemberg", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/elektronische-gesundheitskarte-egk/", + "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Bayern", + "healthCardAndPinPhone": "+498922844050", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Bremen", + "healthCardAndPinPhone": "+4942117610", + "healthCardAndPinMail": "info@hb.aok.de", + "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "AOK Bremerhaven", + "healthCardAndPinPhone": "+49471160", + "healthCardAndPinMail": "info@hb.aok.de", + "healthCardAndPinUrl": "https://www.aok.de/pk/bremen/inhalt/elektronische-gesundheitskarte-3/", + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "AOK - Die Gesundheitskasse Hessen", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/?reg=hessen", + "pinUrl": "https://www.aok.de/pk/versichertenservice/pin-zur-elektronischen-gesundheitskarte/?reg=hessen", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK - Die Gesundheitskasse Niedersachsen", + "healthCardAndPinPhone": "+498000265637", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Nordost", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "eGK_online@nordost.aok.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Nordwest - Die Gesundheitskasse", + "healthCardAndPinPhone": "+498002655060", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK PLUS - Die Gesundheitskasse für Sachsen und Thüringen", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/plus/inhalt/elektronische-gesundheitskarte-anfordern/", + "pinUrl": "https://www.aok.de/pk/plus/inhalt/elektronische-gesundheitskarte-11/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Rheinland/Hamburg - Die Gesundheitskasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "aok@rh.aok.de", + "healthCardAndPinUrl": null, + "pinUrl": "https://www.aok.de/pk/rh/inhalt/pin-zur-elektronischen-gesundheitskarte-egk-5/", + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Rheinland-Pfalz/Saarland - Die Gesundheitskasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.aok.de/pk/rps/inhalt/die-haeufigsten-fragen-und-antworten-zum-e-rezept-4/", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "AOK Sachsen-Anhalt - Die Gesundheitskasse", + "healthCardAndPinPhone": "+4908002265725", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Audi BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.audibkk.de/e-rezept-gematik-egkpin/", + "pinUrl": "https://www.audibkk.de/e-rezept-gematik-pin/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BAHN-BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bahn-bkk.de/egk-erezept", + "pinUrl": "https://www.bahn-bkk.de/egk-erezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Deutsche Bank AG", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkkdb.de/leistungen-beratung/alle-leistungen/alle-leistungen-von-a-z/versichertenkarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BARMER", + "healthCardAndPinPhone": "+498003331010", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.barmer.de/gematik-eRezept", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "DIE BERGISCHE KRANKENKASSE", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bergische-krankenkasse.de/digital/e-rezept", + "pinUrl": "https://www.bergische-krankenkasse.de/digital/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Bertelsmann BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bertelsmann-bkk.de/erezept-egk-pin", + "pinUrl": "https://www.bertelsmann-bkk.de/erezept-egk-pin", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BIG direkt gesund", + "healthCardAndPinPhone": "+4980054565456", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.big-direkt.de/de/erezept-der-gematik-nutzen", + "pinUrl": "https://www.big-direkt.de/de/erezept-der-gematik-nutzen", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Akzo Nobel Bayern", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-akzo.de/service/elektronische-gesundheitskarte-egk", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK B. Braun Aesculap", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-bba.de/egk-pin-bestellen", + "pinUrl": "https://www.bkk-bba.de/pin-bestellen", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK BPW Bergische Achsen KG", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK EUREGIO", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-euregio.de/elektronische-gesundheitskarte", + "pinUrl": "https://www.bkk-euregio.de/elektronische-gesundheitskarte", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK EVM", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK EWE", + "healthCardAndPinPhone": "+49441350285108", + "healthCardAndPinMail": "versicherung@bkk-ewe.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Diakonie", + "healthCardAndPinPhone": "+49521329876120", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-diakonie.de/elektronische-gesundheitskarte/egk-bestellen/", + "pinUrl": "https://www.bkk-diakonie.de/elektronische-gesundheitskarte/egk-bestellen/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK exklusiv", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://bkkexklusiv.de/gesundheitskarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Faber-Castell & Partner", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "erezept@bkk-faber-castell.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK firmus", + "healthCardAndPinPhone": "+4942164343", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-firmus.de/beratung-und-service/online-tools/e-rezept.html", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Freudenberg", + "healthCardAndPinPhone": "+4962016905001", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK GILDEMEISTER SEIDENSTICKER", + "healthCardAndPinPhone": "+498000255255", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkkgs.de/e-rezept", + "pinUrl": "https://www.bkkgs.de/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK GRILLO-WERKE AG", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Groz-Beckert", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "info@bkk-gb.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Herford Minden Ravensberg", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Herkules", + "healthCardAndPinPhone": "+49561208550", + "healthCardAndPinMail": "info@bkk-herkules.de", + "healthCardAndPinUrl": "https://www.bkk-herkules.de/service/gesundheitskarte-und-lichtbild/", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK HMR", + "healthCardAndPinPhone": "+4952211026210", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK KARL MAYER", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Linde", + "healthCardAndPinPhone": "+496117366781", + "healthCardAndPinMail": "egk@bkk-linde.de", + "healthCardAndPinUrl": "https://bkkln.de/epa-egkpin", + "pinUrl": "https://bkkln.de/epa-egkpin", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Melitta HMR", + "healthCardAndPinPhone": "+4957197590", + "healthCardAndPinMail": "info@bkk-melitta.de", + "healthCardAndPinUrl": "https://www.bkk-melitta.de/", + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK MAHLE", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-mahle.de/service/elektronische-gesundheitskarte-egk", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Miele", + "healthCardAndPinPhone": "+498008002189", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.miele-bkk.de/service/elektronische-gesundheitskarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK MTU", + "healthCardAndPinPhone": "+497541907100", + "healthCardAndPinMail": "info@bkk-mtu.de", + "healthCardAndPinUrl": "https://www.bkk-mtu.de/unsere-leistungen/leistungen-a-z/elektronische-gesundheitskarte-egk-bkk-mtu-service/", + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK PFAFF", + "healthCardAndPinPhone": "+49631318760", + "healthCardAndPinMail": "info@bkk-pfaff.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Pfalz", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkkpfalz.de/service-informationen/elektronische-gesundheitskarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK ProVita", + "healthCardAndPinPhone": "+498006648808", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://bkk-provita.de/service-info/e-rezept/", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Public", + "healthCardAndPinPhone": "+495341405600", + "healthCardAndPinMail": "service@bkk-public.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK PricewaterhouseCoopers", + "healthCardAndPinPhone": "+498002557920", + "healthCardAndPinMail": "erezept@bkk-pwc.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK Rieker RICOSTA Weisser", + "healthCardAndPinPhone": "+4974625793030", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK RWE", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkkrwe.de/e-rezept", + "pinUrl": "https://www.bkkrwe.de/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Salzgitter", + "healthCardAndPinPhone": "+495341405700", + "healthCardAndPinMail": "service@bkk-salzgitter.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK SBH", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-sbh.de/e-rezept/", + "pinUrl": "https://bkk-sbh.de/e-rezept/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Scheufelen", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-scheufelen.de/e-rezept", + "pinUrl": "https://www.bkk-scheufelen.de/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Schwarzwald-BaarHeuberg", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK STADT AUGSBURG", + "healthCardAndPinPhone": "+498213243231", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Technoform", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-technoform.de/index.php?p=page&ID=11", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Textilgruppe Hof", + "healthCardAndPinPhone": "+498002558440", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Verkehrsbau Union (VBU)", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "info@bkk-vbu.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK VDN", + "healthCardAndPinPhone": "+49230498260", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK VerbundPlus", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-verbundplus.de/ihre-mitgliedschaft/elektronische-gesundheitskarte/", + "pinUrl": "https://www.bkk-verbundplus.de/nfc-karte-pin", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Voralb", + "healthCardAndPinPhone": "+4970229324639", + "healthCardAndPinMail": "beitraege@bkk-voralb.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK Werra-Meissner", + "healthCardAndPinPhone": "+490565174510", + "healthCardAndPinMail": "info@bkk-wm.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK Wirtschaft & Finanzen", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk-wf.de/e-rezept/", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK Würth", + "healthCardAndPinPhone": "+49794091900", + "healthCardAndPinMail": "info@bkk-wuerth.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "BKK ZF & Partner", + "healthCardAndPinPhone": "+493381306652512", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK_DürkoppAdler", + "healthCardAndPinPhone": "+495215578470", + "healthCardAndPinMail": "eRezept@bkk-da.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BKK24", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bkk24.de/e-rezept", + "pinUrl": "https://bkk24.de/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "BMW BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.bmwbkk.de/egk", + "pinUrl": "https://www.bmwbkk.de/egk-pin-puk", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Bosch BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "info@bosch-bkk.de", + "healthCardAndPinUrl": "https://meine.bosch-bkk.de/bitgo_gs/de/oeffentlich/login/login.xhtml", + "pinUrl": "https://meine.bosch-bkk.de/bitgo_gs/de/oeffentlich/login/login.xhtml", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Continentale BKK", + "healthCardAndPinPhone": "+498006262626", + "healthCardAndPinMail": "kundenservice@continentale-bkk.de", + "healthCardAndPinUrl": "https://www.continentale-bkk.de/kontakt/kontaktformular/", + "pinUrl": "https://www.continentale-bkk.de/kontakt/kontaktformular/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Mercedes-Benz BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.mercedes-benz-bkk.com/service/erezept", + "pinUrl": "https://www.mercedes-benz-bkk.com/service/erezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "DAK-Gesundheit", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.dak.de/dak/ihr-anliegen/elektronische-gesundheitskarte-2083662.html#/", + "pinUrl": "https://www.dak.de/dak/ihr-anliegen/elektronische-gesundheitskarte-2083662.html#/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Debeka BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": "https://www.debeka-bkk.de/erezept/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "energie - Betriebskrankenkasse", + "healthCardAndPinPhone": "+4951191110911", + "healthCardAndPinMail": "info@energie-bkk.de", + "healthCardAndPinUrl": "https://www.energie-bkk.de/das-erezept-8979.html", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Ernst & Young BKK", + "healthCardAndPinPhone": "+495661707670", + "healthCardAndPinMail": "versicherung@ey-bkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "Heimat Krankenkasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.heimat-krankenkasse.de/egk-anfordern", + "pinUrl": "https://www.heimat-krankenkasse.de/egk-pin-anfordern", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "HEK - Hanseatische Krankenkasse", + "healthCardAndPinPhone": "+498000213213", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": "https://www.hek.de/egk", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Handelskrankenkasse (hkk)", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.hkk.de/versicherung-und-tarife/allgemeine-infos/erezept-app", + "pinUrl": "https://www.hkk.de/versicherung-und-tarife/allgemeine-infos/erezept-app", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "IKK - Die Innovationskasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.die-ik.de/e-rezept", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "IKK Brandenburg und Berlin", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.ikkbb.de/erezept/auth-egk", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "IKK classic", + "healthCardAndPinPhone": "+498004551111", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "IKK gesund plus", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "IKK Südwest", + "healthCardAndPinPhone": "+498000119119", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.ikk-suedwest.de/service/persoenlicher-kundenberater/", + "pinUrl": "https://www.ikk-suedwest.de/service/persoenlicher-kundenberater/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Kaufmännische Krankenkasse - KKH", + "healthCardAndPinPhone": "+498005548640554", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "KNAPPSCHAFT", + "healthCardAndPinPhone": "+498000200501", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Koenig & Bauer BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "erezept@koenig-bauer-bkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "Krones BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "Merck BKK", + "healthCardAndPinPhone": "+496151722256", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "mhplus Betriebskrankenkasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.mhplus-krankenkasse.de/privatkunden/unser-service/services-fuer-mitglieder/mhplus-gesundheitskarte-bestellen", + "pinUrl": "https://www.mhplus-krankenkasse.de/privatkunden/unser-service/gesundheit-digital/elektronische-patientenakte/registrierung-fuer-digitale-anwendungen", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Mobil Krankenkasse", + "healthCardAndPinPhone": "+498002550800", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://mobil-krankenkasse.de/unser-service/elektronische-gesundheitskarte/pin-puk-egk.html", + "pinUrl": "https://mobil-krankenkasse.de/unser-service/elektronische-gesundheitskarte/pin-puk-egk.html", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Novitas BKK", + "healthCardAndPinPhone": "+498006566300", + "healthCardAndPinMail": "gesundheitskarte@novitas-bkk.de", + "healthCardAndPinUrl": "https://www.novitas-bkk.de/egk", + "pinUrl": "https://www.novitas-bkk.de/egk", + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "pronova BKK", + "healthCardAndPinPhone": "+49621533911000", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.pronovabkk.de/leistungen/elektronische-gesundheitskarte", + "pinUrl": "https://meine.pronovabkk.de/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "R+V Betriebskrankenkasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.ruv-bkk.de/leistungen/alle-leistungen-im-ueberblick/leistungen-a-z/e/erezept/", + "pinUrl": "https://www.ruv-bkk.de/leistungen/alle-leistungen-im-ueberblick/leistungen-a-z/e/erezept/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Salus BKK", + "healthCardAndPinPhone": "+498002213222", + "healthCardAndPinMail": "egk@salus-bkk.de", + "healthCardAndPinUrl": "https://www.salus-bkk.de/service-formulare/infos-zur-mitgliedschaft/meine-gesundheitskarte/elektronische-gesundheitskarte/", + "pinUrl": "https://www.salus-bkk.de/service-formulare/infos-zur-mitgliedschaft/meine-gesundheitskarte/elektronische-gesundheitskarte/", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "SIEMAG BKK", + "healthCardAndPinPhone": "+492733292929", + "healthCardAndPinMail": "info@siemagbkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Siemens-Betriebskrankenkasse (SBK)", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://meine.sbk.org/pin_gesundheitskarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "SECURVITA BKK", + "healthCardAndPinPhone": "+494033477", + "healthCardAndPinMail": "egk@securvita-bkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "SKD BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.skd-bkk.de/leistungen/26-elektronische-gesundheitskarte-egk/", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Südzucker BKK", + "healthCardAndPinPhone": "+496213285845", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Sozialversicherung für Landwirtschaft, Forsten und Gartenbau (SVLFG)", + "healthCardAndPinPhone": "+495617850", + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://portal.svlfg.de/svlfg-apps/gesundheitskarte", + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "Techniker Krankenkasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.tk.de/techniker/2113848", + "pinUrl": "https://www.tk.de/techniker/2113852", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "TUI BKK", + "healthCardAndPinPhone": "+495341405800", + "healthCardAndPinMail": "service@tui-bkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "VIACTIV BKK", + "healthCardAndPinPhone": "+498002221211", + "healthCardAndPinMail": "service@viactiv.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "vivida bkk", + "healthCardAndPinPhone": "+49800375537555", + "healthCardAndPinMail": "kundencenter@vividabkk.de", + "healthCardAndPinUrl": "https://www.vividabkk.de/de/service/e-rezept-info", + "pinUrl": "https://www.vividabkk.de/de/service/e-rezept-info", + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + }, + { + "name": "Wieland BKK", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": null, + "healthCardAndPinUrl": "https://www.wieland-bkk.de/service/unsere-digitalen-moeglichkeiten/e-rezept", + "pinUrl": "https://www.wieland-bkk.de/service/unsere-digitalen-moeglichkeiten/e-rezept", + "subjectCardAndPinMail": null, + "bodyCardAndPinMail": null, + "subjectPinMail": null, + "bodyPinMail": null + }, + { + "name": "WMF Betriebskrankenkasse", + "healthCardAndPinPhone": null, + "healthCardAndPinMail": "service@wmf-bkk.de", + "healthCardAndPinUrl": null, + "pinUrl": null, + "subjectCardAndPinMail": "#eGKPIN# Bestellung einer NFC-fähigen Gesundheitskarte und PIN", + "bodyCardAndPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nBitte senden Sie mir hierfür eine NFC-fähige Gesundheitskarte zu. Ich benötige zu der Gesundheitskarte auch die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte", + "subjectPinMail": "#PIN# Bestellung einer PIN zur Gesundheitskarte", + "bodyPinMail": "Sehr geehrte Damen und Herren,\nich möchte die E-Rezept-App der gematik nutzen.\n\nName:\nVorname:\nVersichertennummer/ KVNR:\n\nIch benötige zu der Gesundheitskarte die PIN. Bitte senden Sie mir Informationen zu, wie ich die PIN erhalten kann.\n\nMit freundlichen Grüßen\nIhr Versicherter/ Ihre Versicherte" + } +] \ No newline at end of file diff --git a/android/src/main/res/raw/healthcard_lottie.json b/app/android/src/main/res/raw/healthcard_lottie.json similarity index 100% rename from android/src/main/res/raw/healthcard_lottie.json rename to app/android/src/main/res/raw/healthcard_lottie.json diff --git a/android/src/main/res/raw/nfc_positions.json b/app/android/src/main/res/raw/nfc_positions.json similarity index 100% rename from android/src/main/res/raw/nfc_positions.json rename to app/android/src/main/res/raw/nfc_positions.json diff --git a/android/src/main/res/values/colors.xml b/app/android/src/main/res/values/colors.xml similarity index 100% rename from android/src/main/res/values/colors.xml rename to app/android/src/main/res/values/colors.xml diff --git a/android/src/main/res/values/strings_desktop.xml b/app/android/src/main/res/values/strings_desktop.xml similarity index 100% rename from android/src/main/res/values/strings_desktop.xml rename to app/android/src/main/res/values/strings_desktop.xml diff --git a/android/src/main/res/values/strings_kbv_codes.xml b/app/android/src/main/res/values/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values/strings_kbv_codes.xml rename to app/android/src/main/res/values/strings_kbv_codes.xml diff --git a/android/src/main/res/values/themes.xml b/app/android/src/main/res/values/themes.xml similarity index 100% rename from android/src/main/res/values/themes.xml rename to app/android/src/main/res/values/themes.xml diff --git a/android/src/main/res/xml/network_security_config.xml b/app/android/src/main/res/xml/network_security_config.xml similarity index 100% rename from android/src/main/res/xml/network_security_config.xml rename to app/android/src/main/res/xml/network_security_config.xml diff --git a/android/src/test/assets/audit_event_dev.json b/app/android/src/test/assets/audit_event_dev.json similarity index 100% rename from android/src/test/assets/audit_event_dev.json rename to app/android/src/test/assets/audit_event_dev.json diff --git a/android/src/test/assets/communication_bundle.json b/app/android/src/test/assets/communication_bundle.json similarity index 100% rename from android/src/test/assets/communication_bundle.json rename to app/android/src/test/assets/communication_bundle.json diff --git a/android/src/test/assets/empty_audit_event_dev.json b/app/android/src/test/assets/empty_audit_event_dev.json similarity index 100% rename from android/src/test/assets/empty_audit_event_dev.json rename to app/android/src/test/assets/empty_audit_event_dev.json diff --git a/android/src/test/assets/ibm_task_with_kbv.json b/app/android/src/test/assets/ibm_task_with_kbv.json similarity index 100% rename from android/src/test/assets/ibm_task_with_kbv.json rename to app/android/src/test/assets/ibm_task_with_kbv.json diff --git a/android/src/test/assets/kbv_bundle.json b/app/android/src/test/assets/kbv_bundle.json similarity index 100% rename from android/src/test/assets/kbv_bundle.json rename to app/android/src/test/assets/kbv_bundle.json diff --git a/android/src/test/assets/medication_dispense.json b/app/android/src/test/assets/medication_dispense.json similarity index 100% rename from android/src/test/assets/medication_dispense.json rename to app/android/src/test/assets/medication_dispense.json diff --git a/android/src/test/assets/medication_dispense_bundle.json b/app/android/src/test/assets/medication_dispense_bundle.json similarity index 100% rename from android/src/test/assets/medication_dispense_bundle.json rename to app/android/src/test/assets/medication_dispense_bundle.json diff --git a/android/src/test/assets/pharmacy_result_bundle.json b/app/android/src/test/assets/pharmacy_result_bundle.json similarity index 100% rename from android/src/test/assets/pharmacy_result_bundle.json rename to app/android/src/test/assets/pharmacy_result_bundle.json diff --git a/android/src/test/assets/task_bundle.json b/app/android/src/test/assets/task_bundle.json similarity index 100% rename from android/src/test/assets/task_bundle.json rename to app/android/src/test/assets/task_bundle.json diff --git a/android/src/test/assets/task_with_bundle_response.json b/app/android/src/test/assets/task_with_bundle_response.json similarity index 100% rename from android/src/test/assets/task_with_bundle_response.json rename to app/android/src/test/assets/task_with_bundle_response.json diff --git a/android/src/test/assets/task_with_direct_assignment_without_kbv_bundle.json b/app/android/src/test/assets/task_with_direct_assignment_without_kbv_bundle.json similarity index 100% rename from android/src/test/assets/task_with_direct_assignment_without_kbv_bundle.json rename to app/android/src/test/assets/task_with_direct_assignment_without_kbv_bundle.json diff --git a/android/src/test/assets/task_without_kbv_bundle.json b/app/android/src/test/assets/task_without_kbv_bundle.json similarity index 100% rename from android/src/test/assets/task_without_kbv_bundle.json rename to app/android/src/test/assets/task_without_kbv_bundle.json diff --git a/android/src/test/java/android/util/Base64.kt b/app/android/src/test/java/android/util/Base64.kt similarity index 100% rename from android/src/test/java/android/util/Base64.kt rename to app/android/src/test/java/android/util/Base64.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt b/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt similarity index 86% rename from android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt index 6c1ac3e2..aab2eae0 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/CoroutineTestRule.kt @@ -35,10 +35,10 @@ class CoroutineTestRule( ) : TestWatcher() { val dispatchers = object : DispatchProvider { - override val Default: CoroutineDispatcher get() = testDispatcher - override val IO: CoroutineDispatcher get() = testDispatcher - override val Main: CoroutineDispatcher get() = testDispatcher - override val Unconfined: CoroutineDispatcher get() = testDispatcher + override val default: CoroutineDispatcher get() = testDispatcher + override val io: CoroutineDispatcher get() = testDispatcher + override val main: CoroutineDispatcher get() = testDispatcher + override val unconfined: CoroutineDispatcher get() = testDispatcher } override fun starting(description: Description?) { diff --git a/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/consent/model/ConsentMapperTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/consent/usecase/ConsentUseCaseTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/idp/JWTExtensionsTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCaseTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt b/app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/invoice/usecase/TestInvoices.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCaseTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt similarity index 87% rename from android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt index 1a5182af..bdaeb892 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCaseTest.kt @@ -20,7 +20,8 @@ package de.gematik.ti.erp.app.orders.usecase import de.gematik.ti.erp.app.CoroutineTestRule import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile import kotlinx.datetime.Instant import org.junit.Rule import kotlin.test.Test @@ -32,11 +33,11 @@ class OrderUseCaseTest { @Test fun `communication to message - normal`() { - val communication = SyncedTaskData.Communication( + val communication = Communication( taskId = "", orderId = "", communicationId = "CID123456", - profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, + profile = CommunicationProfile.ErxCommunicationReply, sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", @@ -63,11 +64,11 @@ class OrderUseCaseTest { @Test fun `communication to message - payload partially empty`() { - val communication = SyncedTaskData.Communication( + val communication = Communication( taskId = "", orderId = "", communicationId = "CID123456", - profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, + profile = CommunicationProfile.ErxCommunicationReply, sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", @@ -87,11 +88,11 @@ class OrderUseCaseTest { @Test fun `communication to message - payload broken`() { - val communication = SyncedTaskData.Communication( + val communication = Communication( taskId = "", orderId = "", communicationId = "CID123456", - profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, + profile = CommunicationProfile.ErxCommunicationReply, sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", @@ -111,11 +112,11 @@ class OrderUseCaseTest { @Test fun `communication to message - invalid url`() { - val communication = SyncedTaskData.Communication( + val communication = Communication( taskId = "", orderId = "", communicationId = "CID123456", - profile = SyncedTaskData.CommunicationProfile.ErxCommunicationReply, + profile = CommunicationProfile.ErxCommunicationReply, sentOn = Instant.fromEpochSeconds(123456), sender = "ABC123456", recipient = "ABC654321", diff --git a/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/PharmacyDirectCommunicationTest.kt diff --git a/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt new file mode 100644 index 00000000..92406717 --- /dev/null +++ b/app/android/src/test/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyControllerTest.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.ui + +import de.gematik.ti.erp.app.CoroutineTestRule +import de.gematik.ti.erp.app.fhir.model.PharmacyContacts +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals + +@ExperimentalCoroutinesApi +class PharmacySearchViewModelTest { + @get:Rule + val coroutineRule = CoroutineTestRule() + + private lateinit var pharmacyOrderController: PharmacyOrderController + private lateinit var pharmacySearchUseCase: PharmacySearchUseCase + private lateinit var pharmacyOverviewUseCase: PharmacyOverviewUseCase + private lateinit var profileUseCase: ProfilesUseCase + private lateinit var getActiveProfileUseCase: GetActiveProfileUseCase + private lateinit var getOrderStateUseCase: GetOrderStateUseCase + + @Before + fun setUp() { + pharmacySearchUseCase = mockk() + profileUseCase = mockk() + getActiveProfileUseCase = mockk() + pharmacyOverviewUseCase = mockk() + getOrderStateUseCase = mockk() + every { pharmacySearchUseCase.prescriptionDetailsForOrdering(any()) } returns flowOf(orderState) + every { getActiveProfileUseCase.invoke() } returns flowOf(activeProfile) + every { profileUseCase.profiles } returns flowOf(listOf(profile)) + every { getOrderStateUseCase.invoke() } returns flowOf(orderState) + + pharmacyOrderController = PharmacyOrderController( + getActiveProfileUseCase = getActiveProfileUseCase, + pharmacySearchUseCase = pharmacySearchUseCase, + getOrderStateUseCase = getOrderStateUseCase, + scope = TestScope() + ) + + pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) + } + + @Test + fun `order screen state - default`() = runTest { + val result = pharmacyOrderController.updatedOrdersForTest.first() + assertEquals(contacts, result.contact) + assertEquals(prescriptions, result.orders) + } + + @Test + fun `order screen state - select prescriptions`() = runTest { + pharmacyOrderController.onSelectPrescription(prescriptions[0]) + pharmacyOrderController.onSelectPrescription(prescriptions[1]) + pharmacyOrderController.onSelectPrescription(prescriptions[2]) + + pharmacyOrderController.onDeselectPrescription(prescriptions[0]) + + val state = pharmacyOrderController.updatedOrdersForTest.first() + + assertEquals(contacts, state.contact) + assertEquals( + listOf(prescriptions[1], prescriptions[2]), + state.orders + ) + } + + @Test + fun `order screen state - set contacts`() = runTest { + coEvery { pharmacySearchUseCase.saveShippingContact(any()) } answers { Unit } + coEvery { pharmacySearchUseCase.prescriptionDetailsForOrdering("") } returns flowOf( + PharmacyUseCaseData.OrderState( + orders = prescriptions, + contact = contacts + ) + ) + + pharmacyOrderController.onSaveContact(contacts) + + coroutineRule.testDispatcher.scheduler.runCurrent() + coVerify(exactly = 1) { pharmacySearchUseCase.saveShippingContact(contacts) } + + val state = pharmacyOrderController.updatedOrdersForTest.first() + + assertEquals(contacts, state.contact) + assertEquals(prescriptions, state.orders) + } + + companion object { + private val activeProfile = ProfilesUseCaseData.Profile( + id = "test-active-profile", + name = "Active Profile User", + insurance = ProfileInsuranceInformation(), + active = true, + color = ProfilesData.ProfileColorNames.PINK, + lastAuthenticated = null, + ssoTokenScope = null, + image = null, + avatar = ProfilesData.Avatar.PersonalizedImage + ) + private val profile = ProfilesUseCaseData.Profile( + id = "test-inactive-profile", + name = "Inactive Profile User", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), + active = false, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + avatar = ProfilesData.Avatar.PersonalizedImage, + lastAuthenticated = null, + ssoTokenScope = null + ) + + private val prescriptions = listOf( + PharmacyUseCaseData.PrescriptionOrder( + taskId = "A", + accessCode = "1234", + title = "Test", + timestamp = Instant.fromEpochSeconds(0, 0), + substitutionsAllowed = false, + index = 0 + ), + PharmacyUseCaseData.PrescriptionOrder( + taskId = "B", + accessCode = "1234", + title = "Test", + timestamp = Instant.fromEpochSeconds(0, 0), + substitutionsAllowed = false, + index = 0 + + ), + PharmacyUseCaseData.PrescriptionOrder( + taskId = "C", + accessCode = "1234", + title = "Test", + timestamp = Instant.fromEpochSeconds(0, 0), + substitutionsAllowed = false, + index = 0 + + ) + ) + + private val pharmacy = PharmacyUseCaseData.Pharmacy( + id = "pharmacy", + name = "Test - Pharmacy", + address = null, + location = null, + distance = null, + contacts = PharmacyContacts( + phone = "0123456", + mail = "mail@mail.com", + url = "https://website.com", + pickUpUrl = "https://pickup.com", + deliveryUrl = "https://delivery.com", + onlineServiceUrl = "https://online.com" + ), + provides = listOf(), + openingHours = null, + telematikId = "telematik-id" + ) + + private val orderOption = PharmacyScreenData.OrderOption.PickupService + + private val contacts = PharmacyUseCaseData.ShippingContact( + name = "Beate Muster", + line1 = "Friedrichstraße 136", + line2 = "", + postalCode = "10117", + city = "Berlin", + telephoneNumber = "0123456789", + mail = "mail@mail.com", + deliveryInformation = "Bitte!" + ) + + private val orderState = PharmacyUseCaseData.OrderState( + orders = prescriptions, + contact = contacts + ) + } +} diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/prescription/MapperTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidatorTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompletedTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCaseTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt similarity index 84% rename from android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt index d2842e1e..6ab256ac 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCaseTest.kt @@ -21,9 +21,10 @@ package de.gematik.ti.erp.app.profiles.usecase import de.gematik.ti.erp.app.CoroutineTestRule import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.model.ProfilesData -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData -import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import de.gematik.ti.erp.app.protocol.repository.DefaultAuditEventsRepository import io.mockk.MockKAnnotations import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -39,13 +40,13 @@ class ProfilesUseCaseTest { private lateinit var profilesUseCase: ProfilesUseCase @MockK(relaxed = true) - lateinit var profilesRepository: ProfilesRepository + lateinit var profilesRepository: DefaultProfilesRepository @MockK lateinit var idpRepository: IdpRepository @MockK - lateinit var auditEventsRepository: AuditEventsRepository + lateinit var auditEventsRepository: DefaultAuditEventsRepository @get:Rule val coroutineRule = CoroutineTestRule() @@ -53,13 +54,13 @@ class ProfilesUseCaseTest { val profile = ProfilesUseCaseData.Profile( id = "1234567890", name = "Test", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation(), + insurance = ProfileInsuranceInformation(), active = false, color = ProfilesData.ProfileColorNames.PINK, lastAuthenticated = null, ssoTokenScope = null, - personalizedImage = null, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage + image = null, + avatar = ProfilesData.Avatar.PersonalizedImage ) @Before diff --git a/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCaseTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt similarity index 95% rename from android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt index a50f0d24..42d90097 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/DateTimeTest.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.utils +import de.gematik.ti.erp.app.utils.extensions.temporalText import org.junit.Test import java.util.Locale import kotlin.test.assertEquals diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/utils/Proxy.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt similarity index 99% rename from android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt index 260f4baa..645d91eb 100644 --- a/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt +++ b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestData.kt @@ -208,9 +208,11 @@ fun scannedTask( ScannedTaskData.ScannedTask( profileId = "", taskId = taskId, + name = "", accessCode = accessCode, scannedOn = scannedOn, - redeemedOn = redeemedOn + redeemedOn = redeemedOn, + index = 0 ) val testScannedTasks = diff --git a/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt b/app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/utils/TestExtensions.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/vau/TestData.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreIntegrationTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/vau/TruststoreTest.kt diff --git a/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt b/app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt similarity index 100% rename from android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt rename to app/android/src/test/java/de/gematik/ti/erp/app/vau/repository/VauRepositoryTest.kt diff --git a/android/src/test/res/api-responses/taskResponse.txt b/app/android/src/test/res/api-responses/taskResponse.txt similarity index 100% rename from android/src/test/res/api-responses/taskResponse.txt rename to app/android/src/test/res/api-responses/taskResponse.txt diff --git a/android/src/test/res/certlist.json b/app/android/src/test/res/certlist.json similarity index 100% rename from android/src/test/res/certlist.json rename to app/android/src/test/res/certlist.json diff --git a/android/src/test/res/certs/GEM.KOMP-CA10-TEST-ONLY.pem b/app/android/src/test/res/certs/GEM.KOMP-CA10-TEST-ONLY.pem similarity index 100% rename from android/src/test/res/certs/GEM.KOMP-CA10-TEST-ONLY.pem rename to app/android/src/test/res/certs/GEM.KOMP-CA10-TEST-ONLY.pem diff --git a/android/src/test/res/certs/GEM.KOMP-CA11-TEST-ONLY.pem b/app/android/src/test/res/certs/GEM.KOMP-CA11-TEST-ONLY.pem similarity index 100% rename from android/src/test/res/certs/GEM.KOMP-CA11-TEST-ONLY.pem rename to app/android/src/test/res/certs/GEM.KOMP-CA11-TEST-ONLY.pem diff --git a/android/src/test/res/certs/GEM.RCA3-TEST-ONLY.pem b/app/android/src/test/res/certs/GEM.RCA3-TEST-ONLY.pem similarity index 100% rename from android/src/test/res/certs/GEM.RCA3-TEST-ONLY.pem rename to app/android/src/test/res/certs/GEM.RCA3-TEST-ONLY.pem diff --git a/android/src/test/res/certs/c.fd.enc-erp-erpserver-missingAuthKeyId.pem b/app/android/src/test/res/certs/c.fd.enc-erp-erpserver-missingAuthKeyId.pem similarity index 100% rename from android/src/test/res/certs/c.fd.enc-erp-erpserver-missingAuthKeyId.pem rename to app/android/src/test/res/certs/c.fd.enc-erp-erpserver-missingAuthKeyId.pem diff --git a/android/src/test/res/certs/c.fd.enc-erp-erpserver-otherCA.pem b/app/android/src/test/res/certs/c.fd.enc-erp-erpserver-otherCA.pem similarity index 100% rename from android/src/test/res/certs/c.fd.enc-erp-erpserver-otherCA.pem rename to app/android/src/test/res/certs/c.fd.enc-erp-erpserver-otherCA.pem diff --git a/android/src/test/res/certs/c.fd.enc-erp-erpserverReferenz.pem b/app/android/src/test/res/certs/c.fd.enc-erp-erpserverReferenz.pem similarity index 100% rename from android/src/test/res/certs/c.fd.enc-erp-erpserverReferenz.pem rename to app/android/src/test/res/certs/c.fd.enc-erp-erpserverReferenz.pem diff --git a/android/src/test/res/certs/certlist.json b/app/android/src/test/res/certs/certlist.json similarity index 100% rename from android/src/test/res/certs/certlist.json rename to app/android/src/test/res/certs/certlist.json diff --git a/android/src/test/res/certs/certlist_ref.json b/app/android/src/test/res/certs/certlist_ref.json similarity index 100% rename from android/src/test/res/certs/certlist_ref.json rename to app/android/src/test/res/certs/certlist_ref.json diff --git a/android/src/test/res/certs/idp-fd-sig-refimpl-2.pem b/app/android/src/test/res/certs/idp-fd-sig-refimpl-2.pem similarity index 100% rename from android/src/test/res/certs/idp-fd-sig-refimpl-2.pem rename to app/android/src/test/res/certs/idp-fd-sig-refimpl-2.pem diff --git a/android/src/test/res/certs/idp-fd-sig-refimpl-3.pem b/app/android/src/test/res/certs/idp-fd-sig-refimpl-3.pem similarity index 100% rename from android/src/test/res/certs/idp-fd-sig-refimpl-3.pem rename to app/android/src/test/res/certs/idp-fd-sig-refimpl-3.pem diff --git a/android/src/test/res/certs/ocsp_ca_cert.pem b/app/android/src/test/res/certs/ocsp_ca_cert.pem similarity index 100% rename from android/src/test/res/certs/ocsp_ca_cert.pem rename to app/android/src/test/res/certs/ocsp_ca_cert.pem diff --git a/android/src/test/res/certs/ocsp_signer_cert.pem b/app/android/src/test/res/certs/ocsp_signer_cert.pem similarity index 100% rename from android/src/test/res/certs/ocsp_signer_cert.pem rename to app/android/src/test/res/certs/ocsp_signer_cert.pem diff --git a/android/src/test/res/certs/ocsplist_ref.json b/app/android/src/test/res/certs/ocsplist_ref.json similarity index 100% rename from android/src/test/res/certs/ocsplist_ref.json rename to app/android/src/test/res/certs/ocsplist_ref.json diff --git a/app/demo-mode/.gitignore b/app/demo-mode/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/demo-mode/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/demo-mode/build.gradle.kts b/app/demo-mode/build.gradle.kts new file mode 100644 index 00000000..6ecd307c --- /dev/null +++ b/app/demo-mode/build.gradle.kts @@ -0,0 +1,124 @@ +@file:Suppress("UnstableApiUsage") + +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject +import org.owasp.dependencycheck.reporting.ReportGenerator.Format + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.serialization") + id("org.jetbrains.compose") + id("io.realm.kotlin") + id("kotlin-parcelize") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("de.gematik.ti.erp.dependencies") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") +} + +tasks.named("preBuild") { + dependsOn(":ktlint", ":detekt") +} + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +android { + namespace = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.demomode" + defaultConfig { + testApplicationId = "de.gematik.ti.erp.app..demomode.test" + } + kotlinOptions { + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + dependencyCheck { + analyzers.assemblyEnabled = false + suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" + formats = listOf(Format.HTML, Format.XML) + scanConfigurations = configurations.filter { + it.name.startsWith("api") || + it.name.startsWith("implementation") || + it.name.startsWith("kapt") + }.map { it.name } + } + // disable build config for demo-mode since we use only from features. + // If needed we need a new-namespace + buildFeatures { + buildConfig = false + resValues = false + } +} + +dependencies { + implementation(project(":common")) + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + inject { + coroutines { + implementation(coroutinesCore) + implementation(coroutinesAndroid) + } + dateTime { + implementation(datetime) + } + androidX { + implementation(legacySupport) + implementation(appcompat) + implementation(coreKtx) + implementation(datastorePreferences) + implementation(security) + implementation(lifecycleViewmodel) + implementation(lifecycleComposeRuntime) + implementation(lifecycleProcess) + implementation(composeNavigation) + implementation(composeActivity) + implementation(composePaging) + implementation(camerax2) + implementation(cameraxLifecycle) + implementation(cameraxView) + } + accompanist { + implementation(systemUiController) + } + dependencyInjection { + compileOnly(kodeinCompose) + } + logging { + implementation(napier) + } + lottie { + implementation(lottie) + } + serialization { + implementation(kotlinXJson) + } + crypto { + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) + } + compose { + implementation(runtime) + implementation(foundation) + implementation(material) + implementation(materialIconsExtended) + implementation(animation) + implementation(uiTooling) + implementation(preview) + } + tracking { + implementation(contentSquare) + } + } +} + +secrets { + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() + ) "ci-overrides.properties" else "gradle.properties" +} diff --git a/app/demo-mode/src/main/AndroidManifest.xml b/app/demo-mode/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/app/demo-mode/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt new file mode 100644 index 00000000..42444ddb --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeActivity.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode + +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.flow.MutableStateFlow + +abstract class DemoModeActivity : AppCompatActivity() { + + private val isDemoMode by lazy { MutableStateFlow(false) } + + fun setAsDemoMode() { + isDemoMode.value = true + } + + fun cancelDemoMode() { + isDemoMode.value = false + } + + fun isDemoMode() = isDemoMode.value +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt new file mode 100644 index 00000000..575826f2 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/DemoModeIntent.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode + +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Immutable + +@Immutable +object DemoModeIntent { + inline fun intent( + context: Context, + demoModeAction: DemoModeIntentAction + ) = Intent(context, T::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + action = demoModeAction.name + } +} + +inline fun DemoModeIntent.startAppWithDemoMode(activity: ComponentActivity) { + activity.finish() + activity.startActivity( + intent( + context = activity, + demoModeAction = DemoModeIntentAction.DemoModeStarted + ) + ) +} + +inline fun DemoModeIntent.startAppWithNormalMode(activity: ComponentActivity) { + activity.finish() + activity.startActivity( + intent( + context = activity, + demoModeAction = DemoModeIntentAction.DemoModeEnded + ) + ) +} + +enum class DemoModeIntentAction { + DemoModeStarted, + DemoModeEnded +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt new file mode 100644 index 00000000..64eccf09 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/DemoModeDataSource.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource + +import de.gematik.ti.erp.app.demomode.datasource.data.DemoAuditEventInfo +import de.gematik.ti.erp.app.demomode.datasource.data.DemoPharmacyInfo.demoFavouritePharmacy +import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoScannedPrescription.demoScannedTask01 +import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoScannedPrescription.demoScannedTask02 +import de.gematik.ti.erp.app.demomode.datasource.data.DemoPrescriptionInfo.DemoSyncedPrescription.syncedTask +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile01 +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile02 +import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication +import de.gematik.ti.erp.app.idp.api.models.PairingData +import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry +import de.gematik.ti.erp.app.orders.repository.CachedPharmacy +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.days + +const val INDEX_OUT_OF_BOUNDS = -1 + +class DemoModeDataSource { + + /** + * Data sources for the [profiles] created in the demo-mode + */ + val profiles = MutableStateFlow(mutableListOf(demoProfile01, demoProfile02)) + + /** + * Data sources for the [syncedTasks] created in the demo-mode + */ + val syncedTasks = MutableStateFlow( + mutableListOf( + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 1), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Completed, index = 2), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.InProgress, index = 3), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Canceled, index = 4), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 5), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 6), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 7), + syncedTask(demoProfile01.id, status = SyncedTaskData.TaskStatus.Ready, index = 8), + + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Ready, index = 9), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 10), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.Completed, index = 11), + syncedTask(demoProfile02.id, status = SyncedTaskData.TaskStatus.InProgress, index = 12), + ) + ) + + /** + * Data sources for the [scannedTasks] created in the demo-mode + */ + val scannedTasks = MutableStateFlow(mutableListOf(demoScannedTask01, demoScannedTask02)) + + /** + * Data sources for the [favoritePharmacies] created in the demo-mode + */ + val favoritePharmacies = MutableStateFlow(mutableListOf(demoFavouritePharmacy)) + + /** + * Data sources for the [oftenUsedPharmacies] created in the demo-mode + */ + val oftenUsedPharmacies = MutableStateFlow(mutableListOf()) + + /** + * Data sources for the [auditEvents] created in the demo-mode + */ + val auditEvents = MutableStateFlow( + mutableListOf( + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription(), + DemoAuditEventInfo.downloadDispense(), + DemoAuditEventInfo.downloadPrescription() + ) + ) + + /** + * Data sources for the [communications] created in the demo-mode, + * this is used as the source for communication between the user, pharmacy and the doctor + */ + val communications = MutableStateFlow(mutableListOf()) + + /** + * Data source for the a [profileCommunicationLog] communication log that a particular profile has downloaded the information + */ + val profileCommunicationLog = MutableStateFlow(mutableMapOf("no-profile-id" to false)) + + /** + * Data source for the [cachedPharmacies] used for communications + */ + val cachedPharmacies = MutableStateFlow(mutableListOf()) + + /** + * Data source for the connected device [pairedDevices] that will be shown to the user + */ + val pairedDevices = MutableStateFlow( + mutableListOf( + PairingResponseEntry( + name = "Pixel 10", + creationTime = Clock.System.now().minus(10.days).toEpochMilliseconds(), + signedPairingData = "pairing.data" + ) to + PairingData( + subjectPublicKeyInfoOfSecureElement = "subjectPublicKeyInfoOfSecureElement", + keyAliasOfSecureElement = "keyAliasOfSecureElement", + productName = "productName", + serialNumberOfHealthCard = "serialNumberOfHealthCard", + issuerOfHealthCard = "issuerOfHealthCard", + subjectPublicKeyInfoOfHealthCard = "subjectPublicKeyInfoOfHealthCard", + validityUntilOfHealthCard = Clock.System.now().plus(365.days).toEpochMilliseconds() + ) + ) + ) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt new file mode 100644 index 00000000..2e781d86 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoAuditEventInfo.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import kotlinx.datetime.Clock +import java.util.UUID +import kotlin.random.Random +import kotlin.time.Duration +import kotlin.time.DurationUnit.DAYS +import kotlin.time.DurationUnit.HOURS +import kotlin.time.DurationUnit.MINUTES +import kotlin.time.toDuration + +object DemoAuditEventInfo { + + private val randomDurationUnit = listOf(MINUTES, HOURS, DAYS) + private val randomInt = Random.nextInt(10, 60) + + private fun Int.randomDuration(): Duration { + val randomIndex = (randomDurationUnit.indices).random() + val unit = randomDurationUnit[randomIndex] + return this.toDuration(unit) + } + + internal fun downloadDispense(taskId: String = UUID.randomUUID().toString()) = AuditEventData.AuditEvent( + auditId = UUID.randomUUID().toString(), + taskId = taskId, + description = "Max Mustermann dowloaded a medication dispense list", + timestamp = Clock.System.now().minus(randomInt.randomDuration()) + ) + + internal fun downloadPrescription(taskId: String = UUID.randomUUID().toString()) = + AuditEventData.AuditEvent( + auditId = UUID.randomUUID().toString(), + taskId = taskId, + description = "Max Mustermann dowloaded a prescription $taskId", + timestamp = Clock.System.now().minus(randomInt.randomDuration()) + ) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt new file mode 100644 index 00000000..73086360 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoConstants.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes + +object DemoConstants { + internal val randomTimeToday = Clock.System.now().minus((1..20).random().minutes) + internal val pastDate = Clock.System.now().minus((15..100).random().days) + internal val longerRandomTimeToday = Clock.System.now().minus((2..58).random().minutes) + internal const val PHARMACY_TELEMATIK_ID = "3-03.2.1006210000.10.795" + internal const val SYNCED_TASK_PRESET = "110.000.002.345.863" + internal val NOW = Clock.System.now() + internal val START_DATE = Clock.System.now().minus(5.minutes) + internal val EXPIRY_DATE = Clock.System.now().plus(200.days) + internal val SHORT_EXPIRY_DATE = Clock.System.now().plus(20.days) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt new file mode 100644 index 00000000..2811d773 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPharmacyInfo.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import kotlinx.datetime.Clock +import kotlin.time.Duration.Companion.hours + +object DemoPharmacyInfo { + + internal val PHARMACY_NAMES = listOf( + "Apotheke am Zoo", + "City Apotheke", + "Europa Apotheke", + "Gesundheitsapotheke", + "Sonnen Apotheke", + "Vital Apotheke", + "Rosen Apotheke", + "Adler Apotheke", + "Gutenberg Apotheke", + "Bären Apotheke" + ) + + internal val demoFavouritePharmacy = OverviewPharmacyData.OverviewPharmacy( + telematikId = DemoConstants.PHARMACY_TELEMATIK_ID, // actual id, would need change when the pharmacy changes it + isFavorite = true, + usageCount = 2, + lastUsed = Clock.System.now().minus(1.hours), + pharmacyName = "+1 Apotheke", + address = "Brunnenstraße 64\n" + "13355 Berlin" + ) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt new file mode 100644 index 00000000..25fb7a94 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoPrescriptionInfo.kt @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.EXPIRY_DATE +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.NOW +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.SHORT_EXPIRY_DATE +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.SYNCED_TASK_PRESET +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.longerRandomTimeToday +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.randomTimeToday +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.demoProfile01 +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationDispense +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationPZN +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.MedicationRequest +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Organization +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Patient +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Practitioner +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Quantity +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.Ratio +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.utils.FhirTemporal +import java.util.UUID +import kotlin.random.Random + +object DemoPrescriptionInfo { + + private val SYNCED_MEDICATION_NAMES = listOf( + "Ibuprofen 600", "Meloxicam", "Indomethacin", "Celebrex", "Ketoprofen", "Piroxicam", "Etodolac", "Toradol", + "Aspirin", "Voltaren" + ) + + val SCANNED_MEDICINE_NAMES = listOf( + "Lopressor", "Tenormin", "Prinivil", "Vasetoc", "Cozaar", "Norvasc", "Plavix", "Nitrostat", "Tambocor", + "Lanoxin" + ) + + private val DOSAGE = listOf("1-0-1-1", "1-1-1-1", "0-0-0-1", "1-0-1", "0-1-1-0") + + private val STREET_NAMES = listOf( + "Mühlenweg", + "Birkenallee", + "Sonnenstraße", + "Lindenplatz", + "Friedensgasse", + "Bergstraße", + "Am Rosenhain", + "Eichenweg", + "Schlossallee", + "Marktplatz" + ) + + private val POSTAL_CODES = listOf( + "10115", + "20355", + "30880", + "40476", + "50321", + "60232", + "70794", + "81099", + "90210", + "10001" + ) + + private val CITY_NAMES = listOf( + "Berlin", + "Munich (München)", + "Hamburg", + "Cologne (Köln)", + "Frankfurt", + "Stuttgart", + "Düsseldorf", + "Hannover", + "Leipzig", + "Gotha" + ) + + private val FLOORS = listOf( + "Erdgeschoss", + "1. Stock", + "2. Stock", + "3. Stock", + "Dachgeschoss" + ) + + private val PHONE_NUMBERS = listOf( + "+49 123 4567890", + "+49 234 5678901", + "+49 345 6789012", + "+49 456 7890123", + "+49 567 8901234", + "+49 678 9012345", + "+49 789 0123456", + "+49 890 1234567", + "+49 901 2345678", + "+49 012 3456789" + ) + + private val FIRST_NAMES = listOf( + "Hans", + "Anna", + "Max", + "Sophie", + "Lukas", + "Emma", + "Johann", + "Maria", + "Felix", + "Laura" + ) + + private val NAMES = listOf( + "Hans Müller", + "Anna Schmidt", + "Thomas Wagner", + "Sabine Fischer", + "Stefan Becker", + "Petra Schulz", + "Andreas Koch", + "Julia Richter", + "Martin Bauer", + "Laura Hoffmann" + ) + + private val MEDICATION_SPECIALITIES = listOf( + "Fachärztin für Innere Medizin", + "Facharzt für Orthopädie", + "Fachärztin für Augenheilkunde", + "Facharzt für Hals-Nasen-Ohrenheilkunde", + "Fachärztin für Dermatologie", + "Facharzt für Neurologie", + "Fachärztin für Gynäkologie", + "Facharzt für Urologie", + "Fachärztin für Pädiatrie", + "Facharzt für Anästhesiologie" + ) + + private val MEDICAL_PRACTICES = listOf( + "Praxis Erika Mustermann" to "erika@mustermann.de", + "Dr. Müller's Praxis" to "dr.mueller@example.com", + "Gesundheitszentrum Schmidt" to "schmidt@gesundheitszentrum.net", + "Klinik am See" to "info@klinikamsee.de", + "Praxisgemeinschaft Weber & Schmidt" to "weber-schmidt@praxis.de", + "Dr. Wagner & Partner" to "info@wagnerundpartner.com", + "Gesundheitszentrum Sonnenschein" to "info@gesundheit-sonne.de", + "Praxis für Allgemeinmedizin Meier" to "meier@praxismed.de", + "Klinik Rosenpark" to "info@klinikrosenpark.de", + "Dr. Schmidt's Kinderarztpraxis" to "kinderarzt@drschmidt.de" + ) + + private val DOCTORS_NOTES = listOf( + "Patient hat grippeähnliche Symptome und sollte sich ausruhen.", + "Blutdruck im normalen Bereich, Patient sollte regelmäßig Sport treiben.", + "Anpassung der Medikation notwendig, um den Blutzuckerspiegel zu kontrollieren.", + "Patient klagt über Kopfschmerzen, möglicherweise aufgrund von Stress.", + "Regelmäßige Kontrolluntersuchungen werden empfohlen, um den Heilungsverlauf zu überwachen.", + "Verdacht auf Lebensmittelallergie, Patient sollte Tagebuch über Ernährung führen.", + "Weitere Tests erforderlich, um die Ursache der Beschwerden zu ermitteln.", + "Ruhe und ausreichend Schlaf notwendig, um die Genesung zu fördern.", + "Patient leidet unter Rückenschmerzen, Physiotherapie wird empfohlen.", + "Erhöhte Cholesterinwerte festgestellt, Anpassung der Ernährung notwendig." + ) + + /** + * Copied from [de.gematik.ti.erp.app.prescription.repository.KBVCodeMapping.normSizeMapping] + * in feature module + */ + private val normSizeMappings = listOf("KA", "KTP", "N1", "N2", "N3", "NB", "Sonstiges") + + /** + * Copied from [de.gematik.ti.erp.app.prescription.repository.KBVCodeMapping.codeToFormMapping] + * in feature module + */ + private val codeToFormMapping = listOf("AEO", "AUB", "TAB", "TKA", "TLE", "VKA", "XHA") + + private val PERFORMERS = listOf( + "Apotheker", + "Apothekenhelfer", + "Arzt", + "Krankenschwester", + "Selbst" + ) + + internal const val DEMO_MODE_IDENTIFIER = "1234567890" + + internal val PRACTITIONER = Practitioner( + name = NAMES.random(), + qualification = MEDICATION_SPECIALITIES.random(), + practitionerIdentifier = DEMO_MODE_IDENTIFIER + ) + + private val ADDRESS = SyncedTaskData.Address( + line1 = STREET_NAMES.random(), + line2 = FLOORS.random(), + postalCode = POSTAL_CODES.random(), + city = CITY_NAMES.random() + ) + + private fun organization(): Organization { + val item = MEDICAL_PRACTICES.random() + return Organization( + name = item.first, + address = ADDRESS, + uniqueIdentifier = DEMO_MODE_IDENTIFIER, + phone = PHONE_NUMBERS.random(), + mail = item.second + ) + } + + internal val ORGANIZATION = organization() + + internal val PATIENT = Patient( + name = "${FIRST_NAMES.random()} Mustermann", + address = ADDRESS, + birthdate = null, + insuranceIdentifier = DEMO_MODE_IDENTIFIER + ) + + private val RATIO = Ratio( + numerator = Quantity( + value = listOf("1", "2", "3", "4", "5").random(), + unit = "oz" + ), + denominator = null + ) + + private val MEDICATION = MedicationPZN( + category = SyncedTaskData.MedicationCategory.values().random(), + vaccine = Random.nextBoolean(), + text = SYNCED_MEDICATION_NAMES.random(), + form = codeToFormMapping.random(), + lotNumber = DEMO_MODE_IDENTIFIER, + expirationDate = FhirTemporal.Instant(EXPIRY_DATE), + uniqueIdentifier = DEMO_MODE_IDENTIFIER, + normSizeCode = normSizeMappings.random(), + amount = RATIO + ) + + internal val MEDICATION_DISPENSE = MedicationDispense( + dispenseId = UUID.randomUUID().toString(), + patientIdentifier = PATIENT.insuranceIdentifier ?: "", + medication = MEDICATION, + wasSubstituted = false, + dosageInstruction = DOSAGE.random(), + performer = PERFORMERS.random(), + whenHandedOver = null + ) + + internal val MEDICATION_REQUEST = MedicationRequest( + medication = MEDICATION, + dateOfAccident = null, + location = CITY_NAMES.random(), + emergencyFee = Random.nextBoolean(), + dosageInstruction = DOSAGE.random(), + multiplePrescriptionInfo = SyncedTaskData.MultiplePrescriptionInfo(), + note = DOCTORS_NOTES.random(), + substitutionAllowed = Random.nextBoolean() + ) + + internal object DemoScannedPrescription { + internal val demoScannedTask01 = ScannedTaskData.ScannedTask( + profileId = demoProfile01.id, + taskId = "160.000.006.394.157.15", + index = 1, + name = SCANNED_MEDICINE_NAMES.random(), + accessCode = "8cc887c16681517e2db71078f367d4446c156bde743e15c2440722ec0835f406", + scannedOn = randomTimeToday, + redeemedOn = null, + communications = emptyList() + ) + internal val demoScannedTask02 = ScannedTaskData.ScannedTask( + profileId = DemoProfileInfo.demoProfile02.id, + taskId = "160.000.006.386.866.63", + index = 2, + name = SCANNED_MEDICINE_NAMES.random(), + accessCode = "c0967e56ccbcb55ef0851ac9ad3a03dcfbb5ba1934d8d1338290167e348c876f", + scannedOn = randomTimeToday, + redeemedOn = null, + communications = emptyList() + ) + } + + internal object DemoSyncedPrescription { + internal val demoSyncedPrescription01 = SyncedTaskData.SyncedTask(/**/ + profileId = demoProfile01.id, + taskId = "${SYNCED_TASK_PRESET}.1", + isIncomplete = false, + pvsIdentifier = DEMO_MODE_IDENTIFIER, + accessCode = DEMO_MODE_IDENTIFIER, + lastModified = longerRandomTimeToday, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = null, + status = null + ), + expiresOn = EXPIRY_DATE, + acceptUntil = SHORT_EXPIRY_DATE, + authoredOn = NOW, + status = SyncedTaskData.TaskStatus.Ready, + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = listOf(MEDICATION_DISPENSE), + communications = emptyList(), + failureToReport = "" + ) + + internal fun syncedTask( + profileIdentifier: ProfileIdentifier, + status: SyncedTaskData.TaskStatus = SyncedTaskData.TaskStatus.Ready, + index: Int + ) = SyncedTaskData.SyncedTask( + profileId = profileIdentifier, + taskId = "$SYNCED_TASK_PRESET.$index", + isIncomplete = false, + pvsIdentifier = DEMO_MODE_IDENTIFIER, + accessCode = DEMO_MODE_IDENTIFIER, + lastModified = longerRandomTimeToday, + organization = ORGANIZATION, + practitioner = PRACTITIONER, + patient = PATIENT, + insuranceInformation = SyncedTaskData.InsuranceInformation( + name = null, + status = null + ), + expiresOn = EXPIRY_DATE, + acceptUntil = SHORT_EXPIRY_DATE, + authoredOn = NOW, + status = status, + medicationRequest = MEDICATION_REQUEST, + medicationDispenses = listOf(MEDICATION_DISPENSE), + communications = emptyList(), + failureToReport = "" + ) + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt new file mode 100644 index 00000000..0f07270e --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/datasource/data/DemoProfileInfo.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.datasource.data + +import de.gematik.ti.erp.app.BuildKonfig +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.EXPIRY_DATE +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.START_DATE +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.datetime.Instant +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.util.encoders.Base64 +import java.util.UUID + +object DemoProfileInfo { + private const val CAN = "123123" + private val byteArray = Base64.decode(BuildKonfig.DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE) + private val HEALTH_CERTIFICATE = X509CertificateHolder(byteArray) + private val singleSignOnToken = IdpData.SingleSignOnToken( + token = UUID.randomUUID().toString(), + expiresOn = EXPIRY_DATE, + validOn = START_DATE + ) + + // TODO: Add demo mode for different modes of sign-on scopes + private val cardToken = IdpData.DefaultToken( + token = singleSignOnToken, + cardAccessNumber = CAN, + healthCardCertificate = HEALTH_CERTIFICATE + ) + private val HEALTH_INSURANCE_COMPANIES = listOf( + "GesundheitsVersichert AG", + "HeilungsHüter Versicherung", + "VitalSchutz GmbH", + "GesundheitsRundum Versicherung", + "MediSicher Deutschland", + "PflegePlus Versicherungsgruppe", + "GesundheitsVorsorge AG", + "HeilHaus Versicherungen", + "LebenFit Krankenversicherung", + "GesundheitsZirkel Versicherung" + ) + + private fun insuranceNumberGenerator(): String { + val letter = ('A'..'Z').random() + val randomNumber = (10000000..99999999).random() + return "$letter$randomNumber" + } + + internal val demoProfile01 = profile( + profileName = "Erika Mustermann", + isActive = true, + color = ProfilesData.ProfileColorNames.SUN_DEW, + insuranceType = InsuranceTypeV1.PKV, // Note: Private insurance account + avatar = ProfilesData.Avatar.FemaleDoctorWithPhone, + lastAuthenticated = null + ) + + /** + * This [demoProfile02] always starts with orders, so if modifying please take care of that too + */ + internal val demoProfile02 = profile( + profileName = "Max Mustermann", + isActive = false, + insuranceType = InsuranceTypeV1.GKV, + avatar = ProfilesData.Avatar.OldManOfColor, + lastAuthenticated = null + ) + + private fun profile( + profileName: String, + isActive: Boolean = true, + color: ProfilesData.ProfileColorNames = ProfilesData.ProfileColorNames.BLUE_MOON, + avatar: ProfilesData.Avatar = ProfilesData.Avatar.FemaleDeveloper, + insuranceType: InsuranceTypeV1 = InsuranceTypeV1.GKV, + lastAuthenticated: Instant? = null, + singleSignOnTokenScope: IdpData.SingleSignOnTokenScope? = cardToken + ) = ProfilesData.Profile( + id = UUID.randomUUID().toString(), + name = profileName, + color = color, + avatar = avatar, + insuranceIdentifier = insuranceNumberGenerator(), + insuranceType = insuranceType, + insurantName = profileName, + insuranceName = HEALTH_INSURANCE_COMPANIES.random(), + singleSignOnTokenScope = singleSignOnTokenScope, + active = isActive, + lastAuthenticated = lastAuthenticated + ) + + internal fun String.create() = profile(profileName = this) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt new file mode 100644 index 00000000..1ee15dbb --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/di/DemoModeModule.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.di + +import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.mapper.authentication.DemoPromptAuthenticationProvider +import de.gematik.ti.erp.app.demomode.repository.orders.DemoCommunicationRepository +import de.gematik.ti.erp.app.demomode.repository.orders.DemoDownloadCommunicationResource +import de.gematik.ti.erp.app.demomode.repository.pharmacy.DemoPharmacyLocalDataSource +import de.gematik.ti.erp.app.demomode.repository.prescriptions.DemoPrescriptionsRepository +import de.gematik.ti.erp.app.demomode.repository.prescriptions.DemoTaskRepository +import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository +import de.gematik.ti.erp.app.demomode.repository.protocol.DemoAuditEventsRepository +import de.gematik.ti.erp.app.demomode.usecase.idp.DemoIdpUseCase +import de.gematik.ti.erp.app.idp.usecase.IdpUseCase +import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.bindSingleton +import org.kodein.di.instance + +val demoModeModule = DI.Module("demoModeModule") { + bindProvider { DemoDownloadCommunicationResource(instance()) } + // only data source for demo mode + bindSingleton { DemoModeDataSource() } + +} + +fun DI.MainBuilder.demoModeOverrides() { + bindProvider(overrides = true) { DemoProfilesRepository(instance()) } + bindProvider(overrides = true) { DemoPrescriptionsRepository(instance()) } + bindProvider(overrides = true) { DemoAuditEventsRepository(instance()) } + bindProvider(overrides = true) { DemoPharmacyLocalDataSource(instance()) } + bindProvider(overrides = true) { DemoCommunicationRepository(instance(), instance()) } + bindProvider(overrides = true) { DemoTaskRepository() } + bindProvider(overrides = true) { DemoIdpUseCase(instance()) } + // these two are added for future functions + bindProvider(overrides = true) { DemoPromptAuthenticationProvider() } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt new file mode 100644 index 00000000..4f4fed66 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/mapper/authentication/DemoPromptAuthenticationProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.mapper.authentication + +import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator.AuthResult.Authenticated +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DemoPromptAuthenticationProvider : PromptAuthenticationProvider { + override fun mapAuthenticationResult( + id: ProfileIdentifier, + initialAuthenticationData: InitialAuthenticationData, + scope: PromptAuthenticator.AuthScope, + authenticators: List + ): Flow = flowOf(Authenticated) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt new file mode 100644 index 00000000..8189c541 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeProfileLinkedCommunication.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.model + +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import java.util.UUID + +/** + * This is created to link the communication with a particular profile + */ +data class DemoModeProfileLinkedCommunication( + val profileId: String, + val taskId: String, + val communicationId: String, + val orderId: String, + val profile: CommunicationProfile, + val sentOn: Instant, + val sender: String, + val recipient: String, + val payload: String?, + val consumed: Boolean +) + +fun DemoModeProfileLinkedCommunication.toSyncedTaskDataCommunication() = + Communication( + taskId = taskId, + communicationId = communicationId, + orderId = orderId, + profile = profile, + sentOn = sentOn, + sender = sender, + recipient = recipient, + payload = payload, + consumed = consumed + ) + +internal fun DemoModeSentCommunicationJson.toDemoModeProfileLinkedCommunication( + profileId: ProfileIdentifier +) = + DemoModeProfileLinkedCommunication( + profileId = profileId, + communicationId = UUID.randomUUID().toString(), + taskId = basedOn.firstNotNullOfOrNull { it.taskId } ?: "", + orderId = identifier.firstNotNullOfOrNull { it.value } ?: "", + profile = when (meta.isRequest) { + true -> CommunicationProfile.ErxCommunicationDispReq + false -> CommunicationProfile.ErxCommunicationReply + }, + sentOn = Clock.System.now(), + sender = payload.firstNotNullOfOrNull { it.name } ?: "", + recipient = recipient.firstNotNullOfOrNull { it.identifier.value } ?: "", + payload = payload.firstNotNullOfOrNull { it.contentString }, + consumed = false + ) diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt new file mode 100644 index 00000000..ec6fc505 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/model/DemoModeSentCommunicationJson.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.model + +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import kotlinx.serialization.Serializable +import org.json.JSONObject + +@Serializable +data class DemoModeSentCommunicationJson( + val resourceType: String, + val meta: DemoModeSentCommunicationMeta, + val identifier: List, + val status: String, + val basedOn: List, + val recipient: List, + val payload: List +) + +@Serializable +data class DemoModeSentCommunicationMeta( + val profile: List +) { + val isRequest = profile.any { it.contains("GEM_ERP_PR_Communication_DispReq") } +} + +@Serializable +data class DemoModeCommunicationOrderIdIdentifier( + val system: String, + // order-id + val value: String +) + +@Serializable +data class DemoModeCommunicationTaskIdIdentifierReference( + val reference: String +) { + val taskId = reference.split('/').getOrNull(1) // task-id +} + +@Serializable +data class DemoModeCommunicationRecipient( + val identifier: DemoModeCommunicationRecipientBundle +) + +@Serializable +data class DemoModeCommunicationRecipientBundle( + val system: String, + val value: String // telematik.id +) + +@Serializable +data class DemoModeCommunicationPayloadContent( + val contentString: String +) { + private fun jsonObject() = JSONObject(contentString) + val name: String + get() = jsonObject().getString("name") + + val supplyOptionType: String + get() = jsonObject().getString("supplyOptionsType") + + val address: SyncedTaskData.Address + get() { + val item = jsonObject().getString("address").split(',') + return SyncedTaskData.Address( + line1 = item[0], + line2 = item[1], + postalCode = item[2], + city = item[3] + ) + } +} + diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt new file mode 100644 index 00000000..1ce5409d --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoCommunicationRepository.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.repository.orders + +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.demomode.datasource.data.DemoConstants.longerRandomTimeToday +import de.gematik.ti.erp.app.demomode.datasource.data.DemoPharmacyInfo.PHARMACY_NAMES +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo +import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication +import de.gematik.ti.erp.app.demomode.model.toSyncedTaskDataCommunication +import de.gematik.ti.erp.app.orders.repository.CachedPharmacy +import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationDispReq +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile.ErxCommunicationReply +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.random.Random + +class DemoCommunicationRepository( + private val dataSource: DemoModeDataSource, + private val downloadCommunicationResource: DemoDownloadCommunicationResource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : CommunicationRepository { + + private val scope = CoroutineScope(dispatcher) + override val pharmacyCacheError = MutableSharedFlow() + + override suspend fun downloadCommunications(profileId: ProfileIdentifier) = withContext(dispatcher) { + delay(1000) // simulates a network delay of one second + Result.success(Unit) + } + + /** + * Synced task communication + */ + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ) = withContext(dispatcher) { + Result.success(ResourcePaging.ResourceResult(0, Unit)) + } + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + withContext(dispatcher) { + val isSynced = Random.nextBoolean() + when { + isSynced -> longerRandomTimeToday + else -> null + } + } + + override fun loadPharmacies(): Flow> = dataSource.cachedPharmacies + + override suspend fun downloadMissingPharmacy(telematikId: String) { + withContext(dispatcher) { + dataSource.cachedPharmacies.value = dataSource.cachedPharmacies.updateAndGet { cachedPharmacies -> + cachedPharmacies.add( + CachedPharmacy( + telematikId = telematikId, + name = PHARMACY_NAMES.random() + ) + ) + cachedPharmacies + } + } + } + + override fun loadSyncedByTaskId(taskId: String): Flow = + dataSource.syncedTasks.map { syncedTasks -> + syncedTasks.find { it.taskId == taskId } + }.flowOn(dispatcher) + + + override fun loadScannedByTaskId(taskId: String): Flow = + dataSource.scannedTasks.map { scannedTask -> + scannedTask.find { it.taskId == taskId } + }.flowOn(dispatcher) + + override fun loadDispReqCommunications(orderId: String) = + dataSource.communications.mapNotNull { communications -> + communications + .filter { it.orderId == orderId && it.profile == ErxCommunicationDispReq } + .map { it.toSyncedTaskDataCommunication() } + }.flowOn(dispatcher) + + override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> { + return loadOrdersByProfileId(profileId).mapNotNull { communications -> + communications.asSequence().filter { + it.profileId == profileId && it.profile == ErxCommunicationDispReq + } + .map { it.toSyncedTaskDataCommunication() } + .sortedByDescending { it.sentOn } + .distinctBy { it.orderId } + .toList() + }.flowOn(dispatcher) + } + + override fun loadRepliedCommunications(taskIds: List) = + dataSource.communications.mapNotNull { communications -> + taskIds.mapNotNull { taskId -> + communications.find { it.taskId == taskId && it.profile == ErxCommunicationReply } + }.sortedByDescending { it.sentOn } + .map { it.toSyncedTaskDataCommunication() } + }.flowOn(dispatcher) + + override fun hasUnreadPrescription(taskIds: List, orderId: String): Flow = + dataSource.communications.mapNotNull { communications -> + val booleans = taskIds.map { taskId -> + communications.find { it.taskId == taskId && !it.consumed }?.consumed == false + } + booleans.any { it } + }.flowOn(dispatcher) + + override fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow = + dataSource.communications.mapNotNull { communications -> + communications.any { it.profileId == profileId && !it.consumed } + }.flowOn(dispatcher) + + override fun unreadOrders(profileId: ProfileIdentifier): Flow = + dataSource.communications.mapNotNull { communications -> + communications.filter { it.profileId == profileId && !it.consumed } + .distinctBy { it.orderId } + .size.toLong() + } + + override fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = + loadOrdersByProfileId(profileId).mapNotNull { communications -> + communications.count { it.profileId == profileId && !it.consumed }.toLong() + }.flowOn(dispatcher) + + override fun taskIdsByOrder(orderId: String): Flow> = + dataSource.communications.mapNotNull { communications -> + communications.filter { it.orderId == orderId && it.profile == ErxCommunicationDispReq } + .map { it.taskId } + }.flowOn(dispatcher) + + override suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { + withContext(dispatcher) { + dataSource.communications.value = scope.async { + loadOrdersForActiveProfile().mapNotNull { communications -> + communications + .indexOfFirst { it.communicationId == communicationId } + .takeIf { it != INDEX_OUT_OF_BOUNDS } + ?.let { index -> + communications[index] = communications[index].copy( + consumed = consumed + ) + } + communications + } + }.await().first() + } + } + + /** + * Scanned Task communication + * From the scannedTasks get the task for the given [taskId] + * Create a communication object request for the given [taskId] and [transactionId] and save it under + * communications in the scanned-task and save this also in the communications repository + */ + override suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { + withContext(dispatcher) { + val task = dataSource.scannedTasks + .mapNotNull { scannedTasks -> + scannedTasks.find { it.taskId == taskId } + }.first() + val communicationForTask = DemoModeProfileLinkedCommunication( + profileId = task.profileId, + taskId = taskId, + communicationId = transactionId, + sentOn = Clock.System.now(), + sender = pharmacyId, + consumed = false, + profile = ErxCommunicationDispReq, + // these values are kept empty while saving them + orderId = "", + payload = "", + recipient = "" + ) + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { scannedTasks -> + val index = scannedTasks.indexOfFirst { it.taskId == taskId }.takeIf { it != INDEX_OUT_OF_BOUNDS } + index?.let { nonNullIndex -> + scannedTasks[nonNullIndex] = scannedTasks[nonNullIndex].copy( + communications = emptyList() + ) + } + scannedTasks + } + dataSource.communications.value = dataSource.communications.updateAndGet { communications -> + communications.add(communicationForTask) + communications + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun loadOrdersForActiveProfile() = findActiveProfile().flatMapLatest { loadOrdersByProfileId(it.id) } + + + /** + * Method added so that demoModeProfile02 always loads with some communication + * and for other profiles we have to add it. [downloadCommunications] method takes + * in profileId so this can be changed to a different profile later too + */ + private fun loadOrdersByProfileId(profileId: ProfileIdentifier): + Flow> = + // For the profile 2 we load it with some existing communications + if (profileId == DemoProfileInfo.demoProfile02.id) { + dataSource.profileCommunicationLog + .map { it[profileId] } + .flatMapLatest { communicationExists -> + when (communicationExists) { + true -> dataSource.communications + else -> downloadCommunicationResource(profileId = profileId) + } + } + } else { + dataSource.communications + } + + private fun findActiveProfile() = dataSource.profiles.mapNotNull { it.find { profile -> profile.active } } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt new file mode 100644 index 00000000..88fe20b7 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/orders/DemoDownloadCommunicationResource.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.repository.orders + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.demomode.model.DemoModeProfileLinkedCommunication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.datetime.Clock +import java.util.UUID +import kotlin.random.Random +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes + +/** + * Simulates a communication download. + * It gets the existing [dataSource.communications] and [dataSource.syncedTasks] and if the + * [syncedTasks] are redeemable, then by a coin-toss decides if a new communication needs to be + * added to the [dataSource] or not. It then provides this updated list back + */ +class DemoDownloadCommunicationResource( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke( + profileId: ProfileIdentifier + ): Flow> = + combine( + dataSource.communications, + dataSource.syncedTasks + ) { communications, syncedTasks -> + syncedTasks.map { syncedTask -> + // simulates the communication being fetched from the backend + val isCommunicationFetched = Random.nextBoolean() + // coin-toss between simulates between request and reply + val isCommunicationRequest = Random.nextBoolean() + if (syncedTask.redeemState().isRedeemable()) { + val isExisting = communications + .indexOfFirst { it.taskId == syncedTask.taskId } + .takeIf { index -> index != INDEX_OUT_OF_BOUNDS } + isExisting?.let { index -> + val communication = communications[index] + .copy( + profileId = profileId, + recipient = "recipient", + payload = "payload", + consumed = false, + sender = profileId, + sentOn = Clock.System.now().minus(45.minutes), + orderId = UUID.randomUUID().toString(), + profile = when (isCommunicationRequest) { + true -> CommunicationProfile.ErxCommunicationDispReq + false -> CommunicationProfile.ErxCommunicationReply + } + ) + communication to isCommunicationFetched + } ?: run { + val communication = DemoModeProfileLinkedCommunication( + profileId = profileId, + taskId = syncedTask.taskId, + communicationId = UUID.randomUUID().toString(), + orderId = UUID.randomUUID().toString(), + consumed = false, + recipient = "recipient", + payload = "payload", + sender = "sender", + sentOn = Clock.System.now().minus(3.days), + profile = when (isCommunicationRequest) { + true -> CommunicationProfile.ErxCommunicationDispReq + false -> CommunicationProfile.ErxCommunicationReply + } + ) + communication to isCommunicationFetched + } + } else { + null to false + } + } + }.mapNotNull { communicationPairs -> + val communications = dataSource.communications.value + communicationPairs.forEach { communicationPair -> + val (communication, successfulNetworkCall) = communicationPair + + val index = communications.indexOfFirst { item -> + item.taskId == communicationPair.first?.taskId + }.takeIf { it != INDEX_OUT_OF_BOUNDS } + index?.let { nonNullIndex -> + when (successfulNetworkCall && communication != null) { + true -> communications[nonNullIndex] = communication + else -> communications.removeAt(nonNullIndex) + } + } ?: run { + if (successfulNetworkCall && communication != null) { + communications.add(communication) + } + } + } + communications + }.map { communications -> + dataSource.profileCommunicationLog.value = + updateCommunicationLog(dataSource.profileCommunicationLog, profileId) + + dataSource.communications.value = communications + communications + }.flowOn(dispatcher) +} + +/** + * This addition is to be done only once for the profile, this flag is added to not do it twice + */ +private fun updateCommunicationLog( + dataSource: MutableStateFlow>, + profileId: ProfileIdentifier +) = + dataSource.updateAndGet { + it.putIfAbsent(profileId, true) + it + } diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt new file mode 100644 index 00000000..9b434e7c --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/pharmacy/DemoPharmacyLocalDataSource.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.repository.pharmacy + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock + +class DemoPharmacyLocalDataSource( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : PharmacyLocalDataSource { + override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { + withContext(dispatcher) { + dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } + pharmacies + } + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == overviewPharmacy.telematikId } + pharmacies + } + } + } + + override fun loadOftenUsedPharmacies(): Flow> = + dataSource.oftenUsedPharmacies + + override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.oftenUsedPharmacies.value = dataSource.oftenUsedPharmacies.updateAndGet { + val oftenUsedPharmacies = it.toMutableList() + oftenUsedPharmacies.indexOfFirst { item -> item.telematikId == pharmacy.telematikId } + .takeIf { index -> index != INDEX_OUT_OF_BOUNDS } + ?.let { index -> + oftenUsedPharmacies[index] = oftenUsedPharmacies[index].copy( + lastUsed = Clock.System.now(), + usageCount = oftenUsedPharmacies[index].usageCount + 1 + ) + oftenUsedPharmacies + } ?: run { + val isFavourite = dataSource.favoritePharmacies.value + .find { item -> item.telematikId == pharmacy.telematikId } != null + val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( + lastUsed = Clock.System.now(), + usageCount = 1, + isFavorite = isFavourite, + telematikId = pharmacy.telematikId, + pharmacyName = pharmacy.name, + address = pharmacy.address ?: "---" + ) + oftenUsedPharmacies.add(overviewPharmacy) + oftenUsedPharmacies + } + } + } + } + + override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val pharmacies = it.toMutableList() + pharmacies.removeIf { item -> item.telematikId == favoritePharmacy.telematikId } + pharmacies + } + } + } + + override fun loadFavoritePharmacies(): Flow> = + dataSource.favoritePharmacies + + override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { + withContext(dispatcher) { + dataSource.favoritePharmacies.value = dataSource.favoritePharmacies.updateAndGet { + val favoritePharmacies = it.toMutableList() + favoritePharmacies.indexOfFirst { existingPharmacy -> existingPharmacy.telematikId == pharmacy.telematikId } + .takeIf { index -> index != INDEX_OUT_OF_BOUNDS }?.let { index -> + favoritePharmacies[index] = favoritePharmacies[index].copy(lastUsed = Clock.System.now()) + favoritePharmacies + } ?: run { + val overviewPharmacy = OverviewPharmacyData.OverviewPharmacy( + lastUsed = Clock.System.now(), + usageCount = 1, + isFavorite = true, + telematikId = pharmacy.telematikId, + pharmacyName = pharmacy.name, + address = pharmacy.address ?: "---" + ) + favoritePharmacies.add(overviewPharmacy) + favoritePharmacies + } + } + } + } + + override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = + dataSource.favoritePharmacies.mapNotNull { + val favoritePharmacy = it.find { it.telematikId == pharmacy.telematikId } + favoritePharmacy != null + }.flowOn(dispatcher) + + + override suspend fun markAsRedeemed(taskId: String) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedTasks = it.toMutableList() + val index = scannedTasks.indexOfFirst { item -> item.taskId == taskId } + scannedTasks[index] = scannedTasks[index].copy(redeemedOn = Clock.System.now()) + scannedTasks + } + } + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt new file mode 100644 index 00000000..46b5b455 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoPrescriptionsRepository.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.repository.prescriptions + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.demomode.model.DemoModeSentCommunicationJson +import de.gematik.ti.erp.app.demomode.model.toDemoModeProfileLinkedCommunication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData.ScannedTask +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.SyncedTask +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement + +class DemoPrescriptionsRepository( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : PrescriptionRepository { + override suspend fun saveScannedTasks( + profileId: ProfileIdentifier, + tasks: List + ) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + scannedList.addAll(tasks) + scannedList + } + } + } + + override fun scannedTasks(profileId: ProfileIdentifier): Flow> = dataSource.scannedTasks + + override fun syncedTasks(profileId: ProfileIdentifier): Flow> = + dataSource.syncedTasks.mapNotNull { taskList -> + taskList.filter { it.profileId == profileId }.sortedBy { it.lastModified } + }.flowOn(dispatcher) + + + override suspend fun redeemPrescription( + profileId: ProfileIdentifier, + communication: JsonElement, + accessCode: String? + ): Result = + withContext(dispatcher) { + val decodedCommunication = Json + .decodeFromJsonElement(communication) + .toDemoModeProfileLinkedCommunication(profileId) + dataSource.communications.value = dataSource.communications.updateAndGet { communications -> + communications.add(decodedCommunication) + communications + } + Result.success(Unit) + } + + override suspend fun deleteTaskByTaskId(profileId: ProfileIdentifier, taskId: String): Result = + withContext(dispatcher) { + dataSource.syncedTasks.value = dataSource.syncedTasks.updateAndGet { syncedList -> + syncedList.removeIf { it.taskId == taskId && it.profileId == profileId } + syncedList + } + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + scannedList.removeIf { scannedItem -> scannedItem.taskId == taskId && scannedItem.profileId == profileId } + scannedList + } + Result.success(Unit) + } + + // used only for scanned + override suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + val index = scannedList.indexOfFirst { item -> item.taskId == taskId } + if (index != INDEX_OUT_OF_BOUNDS) { + scannedList[index] = scannedList[index].copy(redeemedOn = timestamp) + } + scannedList + } + } + } + + override suspend fun updateScannedTaskName(taskId: String, name: String) { + withContext(dispatcher) { + dataSource.scannedTasks.value = dataSource.scannedTasks.updateAndGet { + val scannedList = it.toMutableList() + val index = scannedList.indexOfFirst { item -> item.taskId == taskId } + if (index != INDEX_OUT_OF_BOUNDS) { + scannedList[index] = scannedList[index].copy(name = name) + } + scannedList + } + } + } + + override fun loadSyncedTaskByTaskId(taskId: String) = + dataSource.syncedTasks.mapNotNull { list -> + list.find { it.taskId == taskId } + }.flowOn(dispatcher) + + override fun loadScannedTaskByTaskId(taskId: String) = + dataSource.scannedTasks.mapNotNull { list -> + list.find { it.taskId == taskId } + }.flowOn(dispatcher) + + override fun loadTaskIds() = + combine( + dataSource.scannedTasks, + dataSource.syncedTasks + ) { scannedTasks, syncedTasks -> + scannedTasks.mapNotNull { it.taskId }.plus( + syncedTasks.mapNotNull { it.taskId } + ) + }.flowOn(dispatcher) +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt new file mode 100644 index 00000000..0f0d7fc1 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/prescriptions/DemoTaskRepository.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.repository.prescriptions + +import de.gematik.ti.erp.app.api.ResourcePaging.ResourceResult +import de.gematik.ti.erp.app.prescription.repository.TaskRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +class DemoTaskRepository( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : TaskRepository { + override suspend fun downloadTasks(profileId: ProfileIdentifier): Result = + withContext(dispatcher) { + delay(500) + Result.success(0) + } + + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> = Result.success(ResourceResult(0, 0)) + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = null + +} + diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt new file mode 100644 index 00000000..88979bb4 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/profiles/DemoProfilesRepository.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +package de.gematik.ti.erp.app.demomode.repository.profiles + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.demomode.datasource.INDEX_OUT_OF_BOUNDS +import de.gematik.ti.erp.app.demomode.datasource.data.DemoProfileInfo.create +import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.Add +import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.NoAction +import de.gematik.ti.erp.app.demomode.repository.profiles.DemoProfilesRepository.ImageActions.Remove +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +class DemoProfilesRepository( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : ProfileRepository { + + override fun profiles() = dataSource.profiles + + override fun activeProfile() = profiles().mapNotNull { + it.find { profile -> profile.active } + } + + override suspend fun saveProfile(profileName: String, activate: Boolean) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + profiles.add(profileName.create()) + profiles + } + } + } + + override suspend fun activateProfile(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val profiles = profileList.deactivateAllProfiles() + val updatedProfiles = profiles.replace(profileId = profileId, activate = true) + updatedProfiles + } + } + } + + override suspend fun removeProfile(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profiles -> + profiles.removeIf { profile -> profile.id == profileId } + profiles + } + } + } + + override suspend fun saveInsuranceInformation( + profileId: ProfileIdentifier, + insurantName: String, + insuranceIdentifier: String, + insuranceName: String + ) { + // Not used in demo mode + } + + override suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { + it.replace(profileId = profileId, name = profileName) + } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun updateProfileColor(profileId: ProfileIdentifier, color: ProfilesData.ProfileColorNames) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { profileList -> + val updatedList = profileList.replace(profileId = profileId, color = color) + updatedList + } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun updateLastAuthenticated(profileId: ProfileIdentifier, lastAuthenticated: Instant) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, lastAuthenticated = lastAuthenticated) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, avatar = avatar) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, profileImage = profileImage, imageAction = Add) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) { + withContext(dispatcher) { + dataSource.profiles.value = dataSource.profiles + .updateAndGet { it.replace(profileId = profileId, profileImage = null, imageAction = Remove) } + .updateUUIDForChangeVisibility() + } + } + + override suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { + // Not for demo mode, will come later + } + + private fun MutableList.index(profileId: ProfileIdentifier) = + indexOfFirst { profile -> profile.id == profileId } + .takeIf { it != INDEX_OUT_OF_BOUNDS } + + private fun MutableList.replace( + profileId: ProfileIdentifier, + activate: Boolean? = null, + name: String? = null, + color: ProfilesData.ProfileColorNames? = null, + lastAuthenticated: Instant? = null, + avatar: ProfilesData.Avatar? = null, + profileImage: ByteArray? = null, + imageAction: ImageActions = NoAction + ): MutableList = + index(profileId)?.let { index -> + val existingProfile = this[index] + this[index] = this[index].copy( + active = activate ?: existingProfile.active, + name = name ?: existingProfile.name, + color = color ?: existingProfile.color, + lastAuthenticated = lastAuthenticated, + avatar = avatar ?: existingProfile.avatar, + personalizedImage = when (imageAction) { + Add -> profileImage + Remove -> null + NoAction -> existingProfile.personalizedImage + } + ) + this + } ?: this + + private fun List.deactivateAllProfiles() = + mapNotNull { + it.copy(active = false) + }.toMutableList() + + private fun MutableList.updateUUIDForChangeVisibility() = this + // map { it.copy(id = UUID.randomUUID().toString()) }.toMutableList() + + enum class ImageActions { + Add, Remove, NoAction + } +} diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt new file mode 100644 index 00000000..f82476bf --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/repository/protocol/DemoAuditEventsRepository.kt @@ -0,0 +1,23 @@ +package de.gematik.ti.erp.app.demomode.repository.protocol + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import java.util.UUID + +class DemoAuditEventsRepository(private val dataSource: DemoModeDataSource) : AuditEventsRepository { + override suspend fun downloadAuditEvents( + profileId: ProfileIdentifier, + count: Int?, + offset: Int? + ): Result { + val mappingResult = AuditEventData.AuditEventMappingResult( + bundleId = UUID.randomUUID().toString(), + bundleResultCount = 3, + auditEvents = dataSource.auditEvents.value + ) + return Result.success(mappingResult) + } + +} \ No newline at end of file diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt new file mode 100644 index 00000000..d30e447d --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/ui/DemoModeTopAppBar.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.ui + +import android.content.Context +import android.content.ContextWrapper +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import de.gematik.ti.erp.app.demomode.DemoModeActivity + +@Suppress("ComposableNaming") +@Composable +fun checkForDemoMode( + demoModeStatusBarColor: Color, + demoModeContent: @Composable ColumnScope.() -> Unit, + appContent: @Composable ColumnScope.() -> Unit +) { + val activity = LocalContext.current.getAsDemoModeActivity() + val isDemoMode = (activity)?.isDemoMode() ?: false + val systemUiController = rememberSystemUiController() + + Column( + modifier = Modifier.fillMaxWidth().statusBarsPadding() + ) { + if (isDemoMode) { + SideEffect { + systemUiController.setStatusBarColor(demoModeStatusBarColor) + } + demoModeContent() + } + appContent() + } +} + +@Composable +fun DemoModeStatusBar( + modifier: Modifier = Modifier, + backgroundColor: Color, + textColor: Color, + demoModeActiveText: String, + demoModeEndText: String, + onClickDemoModeEnd: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = demoModeActiveText, + color = textColor + ) + TextButton(onClick = onClickDemoModeEnd) { + Text( + text = demoModeEndText, + color = textColor, + style = TextStyle(textDecoration = TextDecoration.Underline), + fontWeight = FontWeight.Bold + ) + } + } +} + +private fun Context.getActivity(): AppCompatActivity? { + var currentContext = this + while (currentContext is ContextWrapper) { + if (currentContext is AppCompatActivity) { + return currentContext + } + currentContext = currentContext.baseContext + } + return null +} + +internal fun Context.getAsDemoModeActivity(): DemoModeActivity? = getActivity() as? DemoModeActivity diff --git a/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt new file mode 100644 index 00000000..ed355270 --- /dev/null +++ b/app/demo-mode/src/main/kotlin/de/gematik/ti/erp/app/demomode/usecase/idp/DemoIdpUseCase.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.demomode.usecase.idp + +import de.gematik.ti.erp.app.demomode.datasource.DemoModeDataSource +import de.gematik.ti.erp.app.idp.api.models.AuthenticationId +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.idp.api.models.PairingData +import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry +import de.gematik.ti.erp.app.idp.usecase.IdpUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.withContext +import java.net.URI +import java.security.PublicKey + +class DemoIdpUseCase( + private val dataSource: DemoModeDataSource, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : IdpUseCase { + override suspend fun loadAccessToken( + profileId: ProfileIdentifier, + refresh: Boolean, + scope: IdpScope + ) = "always-give-an-access-token" + + override suspend fun authenticationFlowWithHealthCard( + profileId: ProfileIdentifier, + scope: IdpScope, + cardAccessNumber: String, + healthCardCertificate: suspend () -> ByteArray, + sign: suspend (hash: ByteArray) -> ByteArray + ) { + // no implementation for demo mode + } + + override suspend fun loadExternAuthenticatorIDs(): List = + listOf( + AuthenticationId( + name = "Demo pharmacy 01", + id = "Demo pharmacy id 01" + ), + AuthenticationId( + name = "Demo pharmacy 02", + id = "Demo pharmacy id 02" + ) + ) + + override suspend fun getUniversalLinkForExternalAuthorization( + profileId: ProfileIdentifier, + authenticatorId: String, + authenticatorName: String, + scope: IdpScope + ): URI = URI("https://www.google.com/") + + override suspend fun authenticateWithExternalAppAuthorization(uri: URI) { + // no implementation for demo mode + } + + override suspend fun alternatePairingFlowWithSecureElement( + profileId: ProfileIdentifier, + cardAccessNumber: String, + publicKeyOfSecureElementEntry: PublicKey, + aliasOfSecureElementEntry: ByteArray, + healthCardCertificate: suspend () -> ByteArray, + signWithHealthCard: suspend (hash: ByteArray) -> ByteArray + ) { + // no implementation for demo mode + } + + override suspend fun alternateAuthenticationFlowWithSecureElement(profileId: ProfileIdentifier, scope: IdpScope) { + // no implementation for demo mode + } + + override suspend fun getPairedDevices(profileId: ProfileIdentifier): Result>> = + withContext(dispatcher) { + val device = dataSource.pairedDevices.map { it.toList() }.first() + Result.success(device) + } + + override suspend fun deletePairedDevice(profileId: ProfileIdentifier, deviceAlias: String): Result = + withContext(dispatcher) { + dataSource.pairedDevices.updateAndGet { + it.clear() + it + } + Result.success(Unit) + } +} diff --git a/app/features/.gitignore b/app/features/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/features/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/features/build.gradle.kts b/app/features/build.gradle.kts new file mode 100644 index 00000000..b9dfd63e --- /dev/null +++ b/app/features/build.gradle.kts @@ -0,0 +1,186 @@ +@file:Suppress("UnstableApiUsage") + +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject +import org.owasp.dependencycheck.reporting.ReportGenerator.Format + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.serialization") + id("org.jetbrains.compose") + id("io.realm.kotlin") + id("kotlin-parcelize") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("de.gematik.ti.erp.dependencies") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") +} + +tasks.named("preBuild") { + dependsOn(":ktlint", ":detekt") +} + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +android { + namespace = "${de.gematik.ti.erp.AppDependenciesPlugin.APP_NAME_SPACE}.features" + defaultConfig { + testApplicationId = "de.gematik.ti.erp.app.test" + } + kotlinOptions { + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + dependencyCheck { + analyzers.assemblyEnabled = false + suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" + formats = listOf(Format.HTML, Format.XML) + scanConfigurations = configurations.filter { + it.name.startsWith("api") || + it.name.startsWith("implementation") || + it.name.startsWith("kapt") + }.map { it.name } + } +} + +dependencies { + implementation(project(":common")) + implementation(project(":app:demo-mode")) + testImplementation(project(":common")) + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + testImplementation(kotlin("test")) + implementation("com.tom-roush:pdfbox-android:2.0.27.0") { + exclude(group = "org.bouncycastle") + } + // TODO: Make a common inject for all libs that we don't need to copy again and again + inject { + dataMatrix { + implementation(mlkitBarcodeScanner) + implementation(zxing) + } + coroutines { + implementation(coroutinesCore) + implementation(coroutinesAndroid) + implementation(coroutinesPlayServices) + } + dateTime { + implementation(datetime) + testCompileOnly(datetime) + } + accompanist { + implementation(swipeRefresh) + implementation(flowLayout) + implementation(pager) + implementation(pageIndicator) + implementation(systemUiController) + } + android { + implementation(imageCropper) + debugImplementation(processPhoenix) + } + androidX { + implementation(legacySupport) + implementation(appcompat) + implementation(coreKtx) + implementation(datastorePreferences) + implementation(security) + implementation(biometric) + implementation(webkit) + + implementation(lifecycleViewmodel) + implementation(lifecycleComposeRuntime) + implementation(lifecycleProcess) + implementation(composeNavigation) + implementation(composeActivity) + implementation(composePaging) + implementation(camerax2) + implementation(cameraxLifecycle) + implementation(cameraxView) + } + dependencyInjection { + compileOnly(kodeinCompose) + implementation(kodeinCompose) + androidTestImplementation(kodeinCompose) + } + logging { + implementation(napier) + } + lottie { + implementation(lottie) + } + serialization { + implementation(kotlinXJson) + } + crypto { + implementation(jose4j) + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) + testImplementation(bouncycastleBcprov) + testImplementation(bouncycastleBcpkix) + } + network { + implementation(retrofit) + implementation(retrofit2KotlinXSerialization) + implementation(okhttp3) + implementation(okhttpLogging) + // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). + // Can be removed as soon as Retrofit releases a new version >2.9.0. + implementation(okio) + androidTestImplementation(okhttp3) + } + database { + compileOnly(realm) + testCompileOnly(realm) + } + compose { + implementation(runtime) + implementation(foundation) + implementation(material) + implementation(materialIconsExtended) + implementation(animation) + implementation(uiTooling) + implementation(preview) + } + passwordStrength { + implementation(zxcvbn) + } + tracking { + implementation(contentSquare) + } + maps { + implementation(location) + implementation(maps) + implementation(mapsAndroidUtils) + implementation(mapsKtx) + implementation(mapsCompose) + } + playServices { + implementation(integrity) + implementation(appReview) + implementation(appUpdate) + } + networkTest { + testImplementation(mockWebServer) + } + test { + testImplementation(junit4) + testImplementation(snakeyaml) + testImplementation(json) + testImplementation(mockk) + androidTestImplementation(mockkAndroid) + } + } +} + +secrets { + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() + ) "ci-overrides.properties" else "gradle.properties" +} diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt similarity index 96% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt index 668a2a40..8dff7814 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/DebugSettingsData.kt @@ -41,7 +41,3 @@ data class DebugSettingsData( val virtualHealthCardCert: String, val virtualHealthCardPrivateKey: String ) : Parcelable - -enum class Environment { - PU, TU, RU, RUDEV, TR -} diff --git a/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt new file mode 100644 index 00000000..f02c0fbb --- /dev/null +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/data/Environment.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.debug.data + +enum class Environment { + PU, TU, RU, RUDEV, TR +} diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt similarity index 100% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugLoadingButton.kt diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt similarity index 92% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt index 8289d239..557327c6 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugPKV.kt @@ -39,7 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode @@ -72,13 +72,13 @@ fun DebugScreenPKV(onBack: () -> Unit) { ) { item { DebugCard(title = "Login state") { - val profileHandler = LocalProfileHandler.current - val activeProfile = profileHandler.activeProfile + val profilesController = rememberProfilesController() + val activeProfile by profilesController.getActiveProfileState() Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Button( onClick = { scope.launch { - profileHandler.switchProfileToPKV(activeProfile.id) + profilesController.switchToPrivateInsurance(activeProfile.id) } }, modifier = Modifier.fillMaxWidth() diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt similarity index 99% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt index 5c123ee8..b6bdb198 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreen.kt @@ -76,9 +76,9 @@ import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.debug.data.Environment +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AlertDialog @@ -99,7 +99,6 @@ import java.io.ByteArrayOutputStream import java.time.LocalDateTime import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -import kotlin.math.max @Composable fun DebugCard( diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt similarity index 100% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenNavigation.kt diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt similarity index 100% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt diff --git a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt similarity index 97% rename from android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt index 45b7a327..c4e1561a 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/debug/ui/DebugSettingsViewModel.kt @@ -20,16 +20,18 @@ package de.gematik.ti.erp.app.debug.ui import android.content.Intent import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.gematik.ti.erp.app.App import de.gematik.ti.erp.app.BCProvider import de.gematik.ti.erp.app.BuildKonfig import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.ErezeptApp import de.gematik.ti.erp.app.VisibleDebugTree import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase import de.gematik.ti.erp.app.debug.data.DebugSettingsData @@ -183,8 +185,9 @@ class DebugSettingsViewModel( updateState(debugSettingsData.copy(bearerTokenIsSet = true)) } + @RequiresApi(Build.VERSION_CODES.O) suspend fun breakSSOToken() { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val activeProfileId = profilesUseCase.activeProfileId().first() idpRepository.authenticationData(activeProfileId).first().singleSignOnTokenScope?.let { val newToken = when (it) { @@ -221,6 +224,7 @@ class DebugSettingsViewModel( } } + @RequiresApi(Build.VERSION_CODES.O) private fun IdpData.SingleSignOnToken.breakToken(): IdpData.SingleSignOnToken { val (_, rest) = this.token.split('.', limit = 2) val someHoursBeforeNow = Instant.now().minus(48, ChronoUnit.HOURS).epochSecond @@ -277,7 +281,7 @@ class DebugSettingsViewModel( } private fun restart() { - val context = App.appContext + val context = ErezeptApp.applicationModule.androidContext() val packageManager: PackageManager = context.packageManager val intent = packageManager.getLaunchIntentForPackage(context.packageName) val componentName = intent!!.component @@ -313,7 +317,7 @@ class DebugSettingsViewModel( suspend fun onTriggerVirtualHealthCard( certificateBase64: String, privateKeyBase64: String - ) = withContext(dispatchers.IO) { + ) = withContext(dispatchers.io) { idpUseCase.authenticationFlowWithHealthCard( profileId = profilesUseCase.activeProfileId().first(), cardAccessNumber = "123123", diff --git a/android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt similarity index 100% rename from android/src/debug/java/de/gematik/ti/erp/app/di/EndpointHelper.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/di/EndpointHelper.kt diff --git a/android/src/debug/java/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt similarity index 88% rename from android/src/debug/java/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt rename to app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt index c32a33ea..f358586a 100644 --- a/android/src/debug/java/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt +++ b/app/features/src/debug/kotlin/de/gematik/ti/erp/app/utils/compose/DebugCommon.kt @@ -84,40 +84,40 @@ fun Modifier.visualTestTag(tag: String) = DisposableEffect(tag) { onDispose { - activity.elements -= uuid + activity.elementsUsedInTests -= uuid } } Modifier .testTag(tag) .onGloballyPositioned { - activity.elements += uuid to MainActivity.Element(it.boundsInRoot(), tag) + activity.elementsUsedInTests += uuid to MainActivity.ElementForTest(it.boundsInRoot(), tag) } } @Composable -fun DebugOverlay(elements: Map) { +fun DebugOverlay(elements: Map) { Box(Modifier.fillMaxSize()) { - elements.entries.forEachIndexed { index, (key, el) -> + elements.entries.forEach { (key, elementForTest) -> key(key) { Box( Modifier .layout { measurable, constraints -> val placeable = measurable.measure( Constraints.fixed( - el.bounds.width.toInt(), - el.bounds.height.toInt() + elementForTest.bounds.width.toInt(), + elementForTest.bounds.height.toInt() ) ) layout(placeable.width, placeable.height) { - placeable.place(el.bounds.topLeft.round()) + placeable.place(elementForTest.bounds.topLeft.round()) } } .border(width = 2.dp, color = Color.Magenta, shape = RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp)) ) { Text( - text = el.tag, + text = elementForTest.tag, color = Color.Magenta, overflow = TextOverflow.Visible, modifier = Modifier diff --git a/app/features/src/main/AndroidManifest.xml b/app/features/src/main/AndroidManifest.xml new file mode 100644 index 00000000..021d37c1 --- /dev/null +++ b/app/features/src/main/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/features/src/main/assets/data_terms.html b/app/features/src/main/assets/data_terms.html new file mode 100644 index 00000000..2d2d80be --- /dev/null +++ b/app/features/src/main/assets/data_terms.html @@ -0,0 +1,182 @@ + + + + + Datenschutzerklärung-E-Rezept-App + + + +

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.

+ +

Weitere Informationen

+ +

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

+ +

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

+ +

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

+ +

2. Wozu dient diese App?

+ +

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

+ +

3. Ist die Nutzung der App freiwillig?

+ +

Ja, die Nutzung der E-Rezept-App ist freiwillig. Sie sind auch nicht verpflichtet, bestimmte Daten in der App anzugeben oder sich anzumelden. Wenn Sie sich nicht anmelden, können Sie jedoch nicht alle Funktionen der App nutzen.

+ +

4. Wie und wozu werden Ihre Daten verarbeitet?

+ +

4.1 Profile einrichten

+ +

Sie müssen mindestens ein „Profil“ einrichten.

+ +
    +
  • Die Daten zum Profil werden nur auf Ihrem Gerät gespeichert.
  • +
  • Der Profilname dient der übersichtlichen Anzeige. Er ist frei wählbar.
  • +
  • Sie können mehrere Profile einrichten, um E-Rezepte für Angehörige zu verwalten.
  • +
+ +

4.2 Anmelden am Rezeptdienst

+ +

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

+ +

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

+ +
    +
  • Wenn Sie sich der eGK anmelden, wird das digitale Authentifizierungszertifikat auf dem Chip kontaktlos (per NFC) ausgelesen und an den Identitätsdienst übertragen. Ihre Einwilligung erteilen Sie durch die Eingabe der PIN. Das Authentifizierungszertifikat enthält Ihre Versichertenstammdaten (z. B. Name und Ihre Versichertennummer) und gilt als elektronischer Identitätsnachweis. Sobald der Identitätsdienst das Authentifizierungszertifikat geprüft und Ihre Identität bestätigt hat, stellt er der E-Rezept-App einen Zugangsschlüssel (Token) aus. Mit diesem kann sich die E-Rezept-App am Rezeptdienst anmelden.
  • +
  • Wenn Sie sich mit der ePA-App anmelden, müssen Sie zunächst Ihre Krankenkasse auswählen, damit die E-Rezept-App sich mit der „richtigen“ ePA-App verbinden kann. Die Liste der Krankenkassen ruft die E-Rezept-App vom Identitätsdienst ab. Nachdem Sie Ihre Identitätsdaten über Ihre ePA-App mit der E-Rezept-App geteilt haben, leitet diese die Daten an den Identitätsdienst zur Prüfung weiter. Wenn Ihre Identität bestätigt werden kann, stellt der Identitätsdienst der E-Rezept-App den Zugangsschlüssel (Token) zur Anmeldung am Rezeptdienst aus.
  • +
+ +

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

+ +

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

+ +

4.3 Zugangsdaten speichern

+ +

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

+ +

4.4 Rezeptcodes scannen und einlösen

+ +

Wenn Sie einen Rezeptcode speichern und über die App bei einer Apotheke einlösen wollen, müssen Sie ihn zunächst mit der Kamera scannen. Beim Scannen wird eine elektronische Kopie des Rezeptcodes erzeugt. Dieser Arbeitsschritt findet ausschließlich auf Ihrem Gerät statt, das heißt es wird keine Internetverbindung benötigt. Anschließend können Sie das E-Rezept per App bei einer Apotheke einlösen:

+ +
    +
  • Wenn Sie am Rezeptdienst angemeldet sind und das E-Rezept per App einer Apotheke zur Einlösung zuweisen, wird dies im Rezeptdienst dokumentiert und der Rezeptstatus auf „in Einlösung“ geändert. Die Apotheke erhält dann das Zugriffsrecht für das im Rezeptdienst gespeicherte E-Rezept. Eine direkte Kommunikation zwischen App und Apotheke erfolgt dabei nicht. Sie können den aktuellen Bearbeitungsstatus, der vom Rezeptdienst verwaltet wird, jederzeit in der App einsehen.
  • +
  • Wenn Sie nicht am Rezeptdienst angemeldet sind, können Sie den Rezeptcode zusammen mit Ihren weiteren Bestelldaten (etwa ob Sie das Medikament abholen oder geliefert bekommen möchten) direkt an die Apotheke übermitteln. Die App kommuniziert in diesem Fall direkt mit dem Kommunikationssystem Ihrer Apotheke. Der Rezeptdienst ist an dieser Kommunikation nicht beteiligt und wird daher nicht über die Bestellung informiert. Zu Ihrer Übersicht wird das übermittelte E-Rezept in der Bestellübersicht Ihrer App als „gesendet“ gekennzeichnet. Die Zugangsdaten für das Kommunikationssystem der Apotheke erhält die App vom Apotheken-Verzeichnis.
  • +
+ +Hinweis für Android-Geräte: + +

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

+ +

4.5 Apothekensuche

+ +

Bei Verwendung der Apothekensuche werden Ihre Suchkriterien (z. B. Adressen oder Standortdaten) an das Apotheken-Verzeichnis übermittelt. Das Apotheken-Verzeichnis stellt der App daraufhin eine Liste mit den zu den Suchkriterien passenden Apotheken bereit.

+ +

4.6 Mitteilungen an Apotheken

+ +

Die E-Rezept-App ermöglicht eine direkte verschlüsselte und vertrauliche Kommunikation zwischen Ihnen und Apotheken.

+ +

Wenn Sie am Rezeptdienst angemeldet sind, läuft die Kommunikation mit der Apotheke über den Rezeptdienst, der die Mitteilungen auch archiviert. Die App lädt die Mitteilungen vom Rezeptdienst und speichert sie auf Ihrem Gerät, so dass Sie auch ohne Anmeldung den aktuellen Stand sehen können. Weder der Anbieter des Rezeptdienstes noch die gematik haben Einblick in die ausgetauschten Mitteilungen.

+ +

Wenn Sie nicht am Rezeptdienst angemeldet sind, können Sie über die App Mitteilungen direkt an das Kommunikationssystem Ihrer Apotheke senden. Die Zugangsdaten für das Kommunikationssystem erhält die App vom Apotheken-Verzeichnis. Der Rezeptdienst ist an der Kommunikation nicht beteiligt. Bitte beachten Sie: Um Mitteilungen empfangen und Mitteilungsverläufe einsehen zu können, müssen Sie sich am Rezeptdienst anmelden.

+ +

4.7 Sicherheitsfunktionen

+ +

(a) Verbundene Geräte

+ +

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

+ +

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

+ +

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

+ +

(b) Integritätsprüfungen

+ +

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

+ +

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

+ +

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

+ +

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

+ +

(c) App-Zugriffssperre

+ +

Um unbefugte Zugriffe auf Ihre E-Rezept-App zu erschweren, kann sie nach jedem Schließen automatisch gesperrt werden. Bei jedem Öffnen muss sie dann zunächst entsperrt werden. Je nachdem, über welche Sicherheitsausstattung Ihr Smartphone verfügt, stehen Kennwort-basierte und biometrische Verfahren zur Verfügung.

+ +

Bitte beachten Sie: Wenn Sie die Funktion „Zugangsdaten speichern“ aktivieren, wird automatisch auch die App-Sperre aktiviert.

+ +

Wenn Sie den Kennwortschutz aktivieren, wird Ihr Kennwort während der Einrichtung lokal auf ausreichende Stärke geprüft. Das Kennwort wird nur auf Ihrem Gerät gespeichert.

+ +

Bei Nutzung eines biometrischen Verfahrens werden die Sicherheitsfunktionen Ihres Smartphones verwendet. Die E-Rezept-App erhält dabei keine biometrischen Daten, sondern nur das Ergebnis der biometrischen Prüfung durch Ihr Betriebssystem.

+ +

4.8 Push-Benachrichtigungen

+ +

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

+ +

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

+ +

4.9 Nutzungsanalyse

+ +

Wenn Sie die Nutzungsanalyse erlauben, werden allgemeine Nutzungsdaten (z.B. Art und den Umfang der Nutzung und Bedienung der App-Funktionen) und Angaben zur verwendeten Hard - und Software (z.B. Art, Version und Hersteller des Gerätes und des Betriebssystems) sowie gewählte Einstellungen (z.B. Spracheinstellung) übermittelt. Die Nutzungsdaten werden ausschließlich anonymisiert ausgewertet. Ihre Einwilligung können Sie jederzeit widerrufen, indem Sie die Nutzungsanalyse in den Einstellungen deaktivieren. Ihre Nutzungsdaten werden dann nicht mehr an die gematik übertragen. Weitere Hinweise erhalten Sie vor dem Aktivieren der Nutzungsanalyse in der E-Rezept-App.

+ +

5. An wen werden Ihre Daten weitergegeben?

+ +

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

+ +

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

+ +

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

+ +

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

+ +

6. Wann werden Ihre Daten gelöscht?

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

7. Ihre Datenschutzrechte

+ +

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

+ +

8. Weitere Verantwortliche

+ +

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

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

9. Ansprechpartner

+ +

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

+ +

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

+ + + +

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

+ +

Stand: Juni 2023

+ + + \ No newline at end of file diff --git a/app/features/src/main/assets/open_source_licenses.json b/app/features/src/main/assets/open_source_licenses.json new file mode 100644 index 00000000..6f7cd6b5 --- /dev/null +++ b/app/features/src/main/assets/open_source_licenses.json @@ -0,0 +1,3133 @@ +[ + { + "project": "Accompanist FlowLayout library", + "description": "Utilities for Jetpack Compose", + "version": "0.28.0", + "developers": [ + "Google" + ], + "url": "https://github.com/google/accompanist/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.accompanist:accompanist-flowlayout:0.28.0" + }, + { + "project": "Accompanist Pager Indicators", + "description": "Utilities for Jetpack Compose", + "version": "0.28.0", + "developers": [ + "Google" + ], + "url": "https://github.com/google/accompanist/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.accompanist:accompanist-pager-indicators:0.28.0" + }, + { + "project": "Accompanist Pager layouts", + "description": "Utilities for Jetpack Compose", + "version": "0.28.0", + "developers": [ + "Google" + ], + "url": "https://github.com/google/accompanist/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.accompanist:accompanist-pager:0.28.0" + }, + { + "project": "Accompanist SwipeRefresh library", + "description": "Utilities for Jetpack Compose", + "version": "0.28.0", + "developers": [ + "Google" + ], + "url": "https://github.com/google/accompanist/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.accompanist:accompanist-swiperefresh:0.28.0" + }, + { + "project": "Accompanist System UI Controller library", + "description": "Utilities for Jetpack Compose", + "version": "0.28.0", + "developers": [ + "Google" + ], + "url": "https://github.com/google/accompanist/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.accompanist:accompanist-systemuicontroller:0.28.0" + }, + { + "project": "Activity", + "description": "Provides the base Activity subclass and the relevant hooks to build a composable structure on top.", + "version": "1.7.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.activity:activity:1.7.0" + }, + { + "project": "Activity Compose", + "description": "Compose integration with Activity", + "version": "1.7.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.activity:activity-compose:1.7.0" + }, + { + "project": "Activity Kotlin Extensions", + "description": "Kotlin extensions for \u0027activity\u0027 artifact", + "version": "1.7.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/activity#1.7.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.activity:activity-ktx:1.7.0" + }, + { + "project": "Android App Startup Runtime", + "description": "Android App Startup Runtime", + "version": "1.1.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/startup#1.1.1", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.startup:startup-runtime:1.1.1" + }, + { + "project": "Android AppCompat Library", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.6.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.6.0", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.appcompat:appcompat:1.6.0" + }, + { + "project": "Android Arch-Common", + "description": "Android Arch-Common", + "version": "2.2.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/arch-core#2.2.0", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.arch.core:core-common:2.2.0" + }, + { + "project": "Android Arch-Runtime", + "description": "Android Arch-Runtime", + "version": "2.2.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/arch-core#2.2.0", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.arch.core:core-runtime:2.2.0" + }, + { + "project": "Android DataStore", + "description": "Android DataStore - contains the underlying store used by each serialization method along with components that require an Android dependency", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.datastore:datastore:1.0.0" + }, + { + "project": "Android DataStore Core", + "description": "Android DataStore Core - contains the underlying store used by each serialization method", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.datastore:datastore-core:1.0.0" + }, + { + "project": "Android Emoji2 Compat", + "description": "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters.", + "version": "1.3.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.emoji2:emoji2:1.3.0" + }, + { + "project": "Android Emoji2 Compat view helpers", + "description": "View helpers for Emoji2", + "version": "1.3.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/emoji2#1.3.0", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.emoji2:emoji2-views-helper:1.3.0" + }, + { + "project": "Android Lifecycle Kotlin Extensions", + "description": "Kotlin extensions for \u0027lifecycle\u0027 artifact", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" + }, + { + "project": "Android Lifecycle LiveData", + "description": "Android Lifecycle LiveData", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-livedata:2.6.1" + }, + { + "project": "Android Lifecycle LiveData Core", + "description": "Android Lifecycle LiveData Core", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-livedata-core:2.6.1" + }, + { + "project": "Android Lifecycle Process", + "description": "Android Lifecycle Process", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-process:2.6.1" + }, + { + "project": "Android Lifecycle Runtime", + "description": "Android Lifecycle Runtime", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-runtime:2.6.1" + }, + { + "project": "Android Lifecycle ViewModel", + "description": "Android Lifecycle ViewModel", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-viewmodel:2.6.1" + }, + { + "project": "Android Lifecycle ViewModel Kotlin Extensions", + "description": "Kotlin extensions for \u0027viewmodel\u0027 artifact", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" + }, + { + "project": "Android Lifecycle ViewModel with SavedState", + "description": "Android Lifecycle ViewModel", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1" + }, + { + "project": "Android Lifecycle-Common", + "description": "Android Lifecycle-Common", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-common:2.6.1" + }, + { + "project": "Android Lifecycle-Common for Java 8 Language", + "description": "Android Lifecycle-Common for Java 8 Language", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-common-java8:2.6.1" + }, + { + "project": "Android Navigation Common", + "description": "Android Navigation-Common", + "version": "2.5.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.navigation:navigation-common:2.5.3" + }, + { + "project": "Android Navigation Common Kotlin Extensions", + "description": "Android Navigation-Common-Ktx", + "version": "2.5.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.navigation:navigation-common-ktx:2.5.3" + }, + { + "project": "Android Navigation Runtime", + "description": "Android Navigation-Runtime", + "version": "2.5.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.navigation:navigation-runtime:2.5.3" + }, + { + "project": "Android Navigation Runtime Kotlin Extensions", + "description": "Android Navigation-Runtime-Ktx", + "version": "2.5.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.navigation:navigation-runtime-ktx:2.5.3" + }, + { + "project": "Android Paging-Common", + "description": "Android Paging-Common", + "version": "3.2.0-alpha03", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.2.0-alpha03", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.paging:paging-common:3.2.0-alpha03" + }, + { + "project": "Android Paging-Common Kotlin Extensions", + "description": "Kotlin extensions for \u0027paging-common\u0027 artifact", + "version": "3.1.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/paging#3.1.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.paging:paging-common-ktx:3.1.1" + }, + { + "project": "Android Paging-Compose", + "description": "Compose integration with Paging", + "version": "1.0.0-alpha17", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/paging#1.0.0-alpha17", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.paging:paging-compose:1.0.0-alpha17" + }, + { + "project": "Android Preferences DataStore", + "description": "Android Preferences DataStore", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.datastore:datastore-preferences:1.0.0" + }, + { + "project": "Android Preferences DataStore Core", + "description": "Android Preferences DataStore without the Android Dependencies", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.datastore:datastore-preferences-core:1.0.0" + }, + { + "project": "Android Preferences KTX", + "description": "Kotlin extensions for preferences", + "version": "1.2.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/preference#1.2.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.preference:preference-ktx:1.2.0" + }, + { + "project": "Android Resource Inspection - Annotations", + "description": "Annotation processors for Android resource and layout inspection", + "version": "1.0.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.1", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.resourceinspection:resourceinspection-annotation:1.0.1" + }, + { + "project": "Android Resources Library", + "description": "The Resources Library is a static library that you can add to your Android application in order to use resource APIs that backport the latest APIs to older versions of the platform. Compatible on devices running API 14 or later.", + "version": "1.6.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/appcompat#1.6.0", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.appcompat:appcompat-resources:1.6.0" + }, + { + "project": "Android Support AnimatedVectorDrawable", + "description": "Android Support AnimatedVectorDrawable", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx", + "year": "2015", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.vectordrawable:vectordrawable-animated:1.1.0" + }, + { + "project": "Android Support ExifInterface", + "description": "Android Support ExifInterface", + "version": "1.3.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/exifinterface#1.3.3", + "year": "2016", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.exifinterface:exifinterface:1.3.3" + }, + { + "project": "Android Support Library Annotations", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs.", + "version": "1.5.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.5.0", + "year": "2013", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.annotation:annotation:1.5.0" + }, + { + "project": "Android Support Library Async Layout Inflater", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" + }, + { + "project": "Android Support Library collections", + "description": "Standalone efficient collections.", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.collection:collection:1.1.0" + }, + { + "project": "Android Support Library compat", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.9.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/core#1.9.0", + "year": "2015", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.core:core:1.9.0" + }, + { + "project": "Android Support Library Coordinator Layout", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.coordinatorlayout:coordinatorlayout:1.0.0" + }, + { + "project": "Android Support Library core UI", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.legacy:legacy-support-core-ui:1.0.0" + }, + { + "project": "Android Support Library core utils", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.legacy:legacy-support-core-utils:1.0.0" + }, + { + "project": "Android Support Library Cursor Adapter", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.cursoradapter:cursoradapter:1.0.0" + }, + { + "project": "Android Support Library Custom View", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + }, + { + "project": "Android Support Library Custom View", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.customview:customview:1.1.0" + }, + { + "project": "Android Support Library Document File", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.documentfile:documentfile:1.0.0" + }, + { + "project": "Android Support Library Drawer Layout", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.drawerlayout:drawerlayout:1.0.0" + }, + { + "project": "Android Support Library fragment", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.3.6", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/fragment#1.3.6", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.fragment:fragment:1.3.6" + }, + { + "project": "Android Support Library Interpolators", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.interpolator:interpolator:1.0.0" + }, + { + "project": "Android Support Library loader", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.loader:loader:1.0.0" + }, + { + "project": "Android Support Library Local Broadcast Manager", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0" + }, + { + "project": "Android Support Library media compat", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.media:media:1.0.0" + }, + { + "project": "Android Support Library Print", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.print:print:1.0.0" + }, + { + "project": "Android Support Library Sliding Pane Layout", + "description": "SlidingPaneLayout offers a responsive, two pane layout that automatically switches between overlapping panes on smaller devices to a side by side view on larger devices.", + "version": "1.2.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/slidingpanelayout#1.2.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.slidingpanelayout:slidingpanelayout:1.2.0" + }, + { + "project": "Android Support Library v4", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2011", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.legacy:legacy-support-v4:1.0.0" + }, + { + "project": "Android Support Library View Pager", + "description": "The Support Library is a static library that you can add to your Android application in order to use APIs that are either not available for older platform versions or utility APIs that aren\u0027t a part of the framework APIs. Compatible on devices running API 14 or later.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.viewpager:viewpager:1.0.0" + }, + { + "project": "Android Support RecyclerView", + "description": "Android Support RecyclerView", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/recyclerview#1.2.1", + "year": "2014", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.recyclerview:recyclerview:1.2.1" + }, + { + "project": "Android Support VectorDrawable", + "description": "Android Support VectorDrawable", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx", + "year": "2015", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.vectordrawable:vectordrawable:1.1.0" + }, + { + "project": "Android Tracing", + "description": "Android Tracing", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.tracing:tracing:1.0.0" + }, + { + "project": "Android Transition Support Library", + "description": "Android Transition Support Library", + "version": "1.4.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/transition#1.4.1", + "year": "2016", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.transition:transition:1.4.1" + }, + { + "project": "android-maps-utils", + "description": "Handy extensions to the Google Maps Android API.", + "version": "2.2.3", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-utils", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:android-maps-utils:2.2.3" + }, + { + "project": "AndroidX Autofill", + "description": "AndroidX Autofill", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.autofill:autofill:1.0.0" + }, + { + "project": "AndroidX Futures", + "description": "Androidx implementation of Guava\u0027s ListenableFuture", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/topic/libraries/architecture/index.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.concurrent:concurrent-futures:1.1.0" + }, + { + "project": "AndroidX Preference", + "description": "AndroidX Preference", + "version": "1.2.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/preference#1.2.0", + "year": "2015", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.preference:preference:1.2.0" + }, + { + "project": "AndroidX Security", + "description": "AndroidX Security", + "version": "1.1.0-alpha04", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/security#1.1.0-alpha04", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.security:security-crypto:1.1.0-alpha04" + }, + { + "project": "androidx.customview:poolingcontainer", + "description": "Utilities for listening to the lifecycle of containers that manage their child Views\u0027 lifecycle, such as RecyclerView", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/customview#1.0.0", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.customview:customview-poolingcontainer:1.0.0" + }, + { + "project": "androidx.profileinstaller:profileinstaller", + "description": "Allows libraries to prepopulate ahead of time compilation traces to be read by ART", + "version": "1.3.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/profileinstaller#1.3.0", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.profileinstaller:profileinstaller:1.3.0" + }, + { + "project": "app-update", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:app-update:2.0.1" + }, + { + "project": "app-update-ktx", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:app-update-ktx:2.0.1" + }, + { + "project": "atomicfu", + "description": "AtomicFU utilities", + "version": "0.18.5", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.atomicfu", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:atomicfu-jvm:0.18.5" + }, + { + "project": "AutoValue Annotations", + "description": "Immutable value-type code generation for Java 1.6+.", + "version": "1.6.3", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Apache 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.auto.value:auto-value-annotations:1.6.3" + }, + { + "project": "barcode-scanning", + "description": null, + "version": "17.0.2", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.mlkit:barcode-scanning:17.0.2" + }, + { + "project": "barcode-scanning-common", + "description": null, + "version": "17.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.mlkit:barcode-scanning-common:17.0.0" + }, + { + "project": "Biometric", + "description": "The Biometric library is a static library that you can add to your Android application. It invokes BiometricPrompt on devices running P and greater, and on older devices will show a compat dialog. Compatible on devices running API 14 or later.", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/biometric#1.1.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.biometric:biometric:1.1.0" + }, + { + "project": "Bouncy Castle ASN.1 Extension and Utility APIs", + "description": "The Bouncy Castle Java APIs for ASN.1 extension and utility APIs used to support bcpkix and bctls. This jar contains APIs for JDK 1.8 and up.", + "version": "1.72", + "developers": [ + "The Legion of the Bouncy Castle Inc." + ], + "url": "https://www.bouncycastle.org/java.html", + "year": null, + "licenses": [ + { + "license": "Bouncy Castle Licence", + "license_url": "https://www.bouncycastle.org/licence.html" + } + ], + "dependency": "org.bouncycastle:bcutil-jdk18on:1.72" + }, + { + "project": "Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs", + "description": "The Bouncy Castle Java APIs for CMS, PKCS, EAC, TSP, CMP, CRMF, OCSP, and certificate generation. This jar contains APIs for JDK 1.8 and up. The APIs can be used in conjunction with a JCE/JCA provider such as the one provided with the Bouncy Castle Cryptography APIs.", + "version": "1.72", + "developers": [ + "The Legion of the Bouncy Castle Inc." + ], + "url": "https://www.bouncycastle.org/java.html", + "year": null, + "licenses": [ + { + "license": "Bouncy Castle Licence", + "license_url": "https://www.bouncycastle.org/licence.html" + } + ], + "dependency": "org.bouncycastle:bcpkix-jdk18on:1.72" + }, + { + "project": "Bouncy Castle Provider", + "description": "The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.8 and up.", + "version": "1.72", + "developers": [ + "The Legion of the Bouncy Castle Inc." + ], + "url": "https://www.bouncycastle.org/java.html", + "year": null, + "licenses": [ + { + "license": "Bouncy Castle Licence", + "license_url": "https://www.bouncycastle.org/licence.html" + } + ], + "dependency": "org.bouncycastle:bcprov-jdk18on:1.72" + }, + { + "project": "C Interop", + "description": "Wrapper for interacting with Realm Kotlin native code. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", + "developers": [ + "Realm" + ], + "url": "https://realm.io", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "io.realm.kotlin:cinterop-android:1.7.1" + }, + { + "project": "CanHub/Android-Image-Cropper", + "description": "Image Cropping Library for Android, optimised for Camera / Gallery.", + "version": "4.3.2", + "developers": [ + "CanHub" + ], + "url": "https://canhub.github.io/", + "year": "2020", + "licenses": [ + { + "license": "Apache License 2.0", + "license_url": "https://api.github.com/licenses/apache-2.0" + } + ], + "dependency": "com.github.CanHub:Android-Image-Cropper:4.3.2" + }, + { + "project": "Collections Kotlin Extensions", + "description": "Kotlin extensions for \u0027collection\u0027 artifact", + "version": "1.1.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.collection:collection-ktx:1.1.0" + }, + { + "project": "common", + "description": null, + "version": "18.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.mlkit:common:18.0.0" + }, + { + "project": "Compose Animation", + "description": "Compose animation library", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.animation:animation:1.4.2" + }, + { + "project": "Compose Animation Core", + "description": "Animation engine and animation primitives that are the building blocks of the Compose animation library", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-animation#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.animation:animation-core:1.4.2" + }, + { + "project": "Compose Foundation", + "description": "Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.2", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.foundation:foundation:1.4.2" + }, + { + "project": "Compose Geometry", + "description": "Compose classes related to dimensions without units", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-geometry:1.4.2" + }, + { + "project": "Compose Graphics", + "description": "Compose graphics", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-graphics:1.4.2" + }, + { + "project": "Compose Layouts", + "description": "Compose layout implementations", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.foundation:foundation-layout:1.4.2" + }, + { + "project": "Compose Material Components", + "description": "Compose Material Design Components library", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.material:material:1.4.2" + }, + { + "project": "Compose Material Icons Core", + "description": "Compose Material Design core icons. This module contains the most commonly used set of Material icons.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.material:material-icons-core:1.4.2" + }, + { + "project": "Compose Material Icons Extended", + "description": "Compose Material Design extended icons. This module contains all Material icons. It is a very large dependency and should not be included directly.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.material:material-icons-extended:1.4.2" + }, + { + "project": "Compose Material Ripple", + "description": "Material ripple used to build interactive components", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-material#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.material:material-ripple:1.4.2" + }, + { + "project": "Compose Navigation", + "description": "Compose integration with Navigation", + "version": "2.5.3", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/navigation#2.5.3", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.navigation:navigation-compose:2.5.3" + }, + { + "project": "Compose Runtime", + "description": "Tree composition support for code generated by the Compose compiler plugin and corresponding public API", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.runtime:runtime:1.4.2" + }, + { + "project": "Compose Saveable", + "description": "Compose components that allow saving and restoring the local ui state", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.runtime:runtime-saveable:1.4.2" + }, + { + "project": "Compose Tooling", + "description": "Compose tooling library. This library exposes information to our tools for better IDE support.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-tooling:1.4.2" + }, + { + "project": "Compose Tooling API", + "description": "Compose tooling library API. This library provides the API required to declare @Preview composables in user apps.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-tooling-preview:1.4.2" + }, + { + "project": "Compose Tooling Data", + "description": "Compose tooling library data. This library provides data about compose for different tooling purposes.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-tooling-data:1.4.2" + }, + { + "project": "Compose UI primitives", + "description": "Compose UI primitives. This library contains the primitives that form the Compose UI Toolkit, such as drawing, measurement and layout.", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui:1.4.2" + }, + { + "project": "Compose UI Text", + "description": "Compose Text primitives and utilities", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-text:1.4.2" + }, + { + "project": "Compose Unit", + "description": "Compose classes for simple units", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-unit:1.4.2" + }, + { + "project": "Compose Util", + "description": "Internal Compose utilities used by other modules", + "version": "1.4.2", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/compose-ui#1.4.2", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.compose.ui:ui-util:1.4.2" + }, + { + "project": "ContentSquare Android SDK", + "description": "ContentSquareAndroid SDK", + "version": "4.15.0", + "developers": [ + "ContentSquare" + ], + "url": "https://docs.contentsquare.com/android/", + "year": null, + "licenses": [], + "dependency": "com.contentsquare.android:library:4.15.0" + }, + { + "project": "Core Kotlin Extensions", + "description": "Kotlin extensions for \u0027core\u0027 artifact", + "version": "1.9.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/core#1.9.0", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.core:core-ktx:1.9.0" + }, + { + "project": "core-common", + "description": null, + "version": "2.0.2", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:core-common:2.0.2" + }, + { + "project": "Experimental annotation", + "description": "Java annotation for use on unstable Android API surfaces. When used in conjunction with the Experimental annotation lint checks, this annotation provides functional parity with Kotlin\u0027s Experimental annotation.", + "version": "1.3.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/annotation#1.3.0", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.annotation:annotation-experimental:1.3.0" + }, + { + "project": "firebase-annotations", + "description": null, + "version": "16.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.firebase:firebase-annotations:16.0.0" + }, + { + "project": "firebase-components", + "description": null, + "version": "16.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.firebase:firebase-components:16.1.0" + }, + { + "project": "firebase-encoders", + "description": null, + "version": "16.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.firebase:firebase-encoders:16.1.0" + }, + { + "project": "firebase-encoders-json", + "description": null, + "version": "17.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.firebase:firebase-encoders-json:17.1.0" + }, + { + "project": "Fragment Kotlin Extensions", + "description": "Kotlin extensions for \u0027fragment\u0027 artifact", + "version": "1.3.6", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/fragment#1.3.6", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.fragment:fragment-ktx:1.3.6" + }, + { + "project": "Gson", + "description": null, + "version": "2.8.9", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Apache-2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.code.gson:gson:2.8.9" + }, + { + "project": "Guava ListenableFuture only", + "description": "Contains Guava\u0027s com.google.common.util.concurrent.ListenableFuture class,\n without any of its other classes -- but is also available in a second\n \"version\" that omits the class to avoid conflicts with the copy in Guava\n itself. The idea is:\n\n - If users want only ListenableFuture, they depend on listenablefuture-1.0.\n\n - If users want all of Guava, they depend on guava, which, as of Guava\n 27.0, depends on\n listenablefuture-9999.0-empty-to-avoid-conflict-with-guava. The 9999.0-...\n version number is enough for some build systems (notably, Gradle) to select\n that empty artifact over the \"real\" listenablefuture-1.0 -- avoiding a\n conflict with the copy of ListenableFuture in guava itself. If users are\n using an older version of Guava or a build system other than Gradle, they\n may see class conflicts. If so, they can solve them by manually excluding\n the listenablefuture artifact or manually forcing their build systems to\n use 9999.0-....", + "version": "1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.guava:listenablefuture:1.0" + }, + { + "project": "image", + "description": null, + "version": "1.0.0-beta1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.odml:image:1.0.0-beta1" + }, + { + "project": "integrity", + "description": null, + "version": "1.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Integrity API Terms of Service", + "license_url": "https://developer.android.com/google/play/integrity/overview#tos" + } + ], + "dependency": "com.google.android.play:integrity:1.1.0" + }, + { + "project": "IntelliJ IDEA Annotations", + "description": "A set of annotations used for code inspection support and code documentation.", + "version": "13.0", + "developers": [ + "JetBrains Team" + ], + "url": "http://www.jetbrains.org", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains:annotations:13.0" + }, + { + "project": "javax.inject", + "description": "The javax.inject API", + "version": "1", + "developers": [], + "url": "http://code.google.com/p/atinject/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "javax.inject:javax.inject:1" + }, + { + "project": "Jetpack Camera Core Library", + "description": "Core components for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, + { + "license": "BSD License", + "license_url": "https://chromium.googlesource.com/libyuv/libyuv/+/refs/heads/main/README.chromium" + } + ], + "dependency": "androidx.camera:camera-core:1.2.1" + }, + { + "project": "Jetpack Camera Library Camera2 Implementation/Extensions", + "description": "Camera2 implementation and extensions for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.camera:camera-camera2:1.2.1" + }, + { + "project": "Jetpack Camera Lifecycle Library", + "description": "Lifecycle components for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.camera:camera-lifecycle:1.2.1" + }, + { + "project": "Jetpack Camera View Library", + "description": "UI tools for the Jetpack Camera Library, a library providing a consistent and reliable camera foundation that enables great camera driven experiences across all of Android.", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/camera#1.2.1", + "year": "2019", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.camera:camera-view:1.2.1" + }, + { + "project": "Jetpack WindowManager Library", + "description": "WindowManager Jetpack library. Currently only provides additional functionality on foldable devices.", + "version": "1.0.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/window#1.0.0", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.window:window:1.0.0" + }, + { + "project": "JNI Swig Stubs", + "description": "Wrapper for interacting with Realm Kotlin native code from the JVM. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", + "developers": [ + "Realm" + ], + "url": "https://realm.io", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "io.realm.kotlin:jni-swig-stub:1.7.1" + }, + { + "project": "jose4j", + "description": "The jose.4.j library is a robust and easy to use open source implementation of JSON Web Token (JWT) and the JOSE specification suite (JWS, JWE, and JWK).\n It is written in Java and relies solely on the JCA APIs for cryptography.\n Please see https://bitbucket.org/b_c/jose4j/wiki/Home for more info, examples, etc..", + "version": "0.9.2", + "developers": [ + "Brian Campbell" + ], + "url": "https://bitbucket.org/b_c/jose4j/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.bitbucket.b_c:jose4j:0.9.2" + }, + { + "project": "kbson", + "description": "KBSON a kotlin multiplatform implementation of the BSON library.", + "version": "0.2.0", + "developers": [ + "" + ], + "url": "http://www.mongodb.org", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.mongodb.kbson:kbson-android:0.2.0" + }, + { + "project": "Kodein", + "description": "Kodein Core", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-jvm:7.16.0" + }, + { + "project": "Kodein-Framework-Android", + "description": "Standard Kodein classes \u0026 extensions for Android", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-framework-android-core:7.16.0" + }, + { + "project": "Kodein-Framework-Android", + "description": "Kodein extensions with AndroidX compatibility", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-framework-android-x:7.16.0" + }, + { + "project": "Kodein-Framework-AndroidX-ViewModel", + "description": "Kodein extensions for AndroidX ViewModel", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel:7.16.0" + }, + { + "project": "Kodein-Framework-AndroidX-ViewModel-SavedState", + "description": "Kodein extensions for AndroidX ViewModel with SavedStateHandle", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-framework-android-x-viewmodel-savedstate:7.16.0" + }, + { + "project": "Kodein-Framework-Compose", + "description": "Kodein extensions for Jetpack / JetBrains Compose", + "version": "7.16.0", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.di:kodein-di-framework-compose-android:7.16.0" + }, + { + "project": "Kodein-Type", + "description": "Kodein Type System", + "version": "2.2.1", + "developers": [ + "Kodein Koders" + ], + "url": "http://kodein.org", + "year": null, + "licenses": [ + { + "license": "MIT", + "license_url": "https://opensource.org/licenses/MIT" + } + ], + "dependency": "org.kodein.type:kaverit-jvm:2.2.1" + }, + { + "project": "Kotlin Android Extensions Runtime", + "description": "Kotlin Android Extensions Runtime", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.8.10" + }, + { + "project": "Kotlin Reflect", + "description": "Kotlin Full Reflection Library", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-reflect:1.8.10" + }, + { + "project": "Kotlin Stdlib", + "description": "Kotlin Standard Library for JVM", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-stdlib:1.8.10" + }, + { + "project": "Kotlin Stdlib Common", + "description": "Kotlin Common Standard Library", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-common:1.8.10" + }, + { + "project": "Kotlin Stdlib Jdk7", + "description": "Kotlin Standard Library JDK 7 extension", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10" + }, + { + "project": "Kotlin Stdlib Jdk8", + "description": "Kotlin Standard Library JDK 8 extension", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10" + }, + { + "project": "kotlinx-coroutines-android", + "description": "Coroutines support libraries for Kotlin", + "version": "1.6.4", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" + }, + { + "project": "kotlinx-coroutines-core", + "description": "Coroutines support libraries for Kotlin", + "version": "1.6.4", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4" + }, + { + "project": "kotlinx-coroutines-play-services", + "description": "Coroutines support libraries for Kotlin", + "version": "1.6.4", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.coroutines", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4" + }, + { + "project": "kotlinx-datetime", + "description": "Kotlin Datetime Library", + "version": "0.4.0", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx-datetime", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.4.0" + }, + { + "project": "kotlinx-serialization-core", + "description": "Kotlin multiplatform serialization runtime library", + "version": "1.4.1", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.serialization", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.4.1" + }, + { + "project": "kotlinx-serialization-json", + "description": "Kotlin multiplatform serialization runtime library", + "version": "1.4.1", + "developers": [ + "JetBrains Team" + ], + "url": "https://github.com/Kotlin/kotlinx.serialization", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1" + }, + { + "project": "Library", + "description": "Library code for Realm Kotlin. This artifact is not supposed to be consumed directly, but through \u0027io.realm.kotlin:gradle-plugin:1.7.1\u0027 instead.", + "version": "1.7.1", + "developers": [ + "Realm" + ], + "url": "https://realm.io", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "io.realm.kotlin:library-base-android:1.7.1" + }, + { + "project": "Lifecycle ViewModel Compose", + "description": "Compose integration with Lifecycle ViewModel", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2021", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" + }, + { + "project": "LiveData Core Kotlin Extensions", + "description": "Kotlin extensions for \u0027livedata-core\u0027 artifact", + "version": "2.6.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.1" + }, + { + "project": "Lottie", + "description": "Lottie is an animation library that renders Adobe After Effects animations natively in realtime.", + "version": "5.2.0", + "developers": [ + "Airbnb" + ], + "url": "https://github.com/airbnb/lottie-android", + "year": "2017", + "licenses": [ + { + "license": "Apache-2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0" + } + ], + "dependency": "com.airbnb.android:lottie:5.2.0" + }, + { + "project": "Lottie Compose", + "description": "Lottie for Jetpack Compose.", + "version": "5.2.0", + "developers": [ + "Airbnb" + ], + "url": "https://github.com/airbnb/lottie-android", + "year": "2020", + "licenses": [ + { + "license": "Apache-2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0" + } + ], + "dependency": "com.airbnb.android:lottie-compose:5.2.0" + }, + { + "project": "maps-compose", + "description": "Jetpack Compose components for the Maps SDK for Android", + "version": "2.9.1", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-compose", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-compose:2.9.1" + }, + { + "project": "maps-ktx", + "description": "Kotlin extensions (KTX) for Google Maps SDK", + "version": "3.4.0", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-ktx", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-ktx:3.4.0" + }, + { + "project": "maps-utils-ktx", + "description": "Kotlin extensions (KTX) for Google Maps SDK", + "version": "3.4.0", + "developers": [ + "Google Inc." + ], + "url": "https://github.com/googlemaps/android-maps-ktx", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.maps.android:maps-utils-ktx:3.4.0" + }, + { + "project": "napier", + "description": "Kotlin Multiplatform libraries that show logs in common module.", + "version": "2.6.1", + "developers": [ + "aakira" + ], + "url": "https://github.com/aakira/Napier", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "io.github.aakira:napier-android:2.6.1" + }, + { + "project": "okhttp", + "description": "Square’s meticulous HTTP client for Java and Kotlin.", + "version": "4.10.0", + "developers": [ + "Square, Inc." + ], + "url": "https://square.github.io/okhttp/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.squareup.okhttp3:okhttp:4.10.0" + }, + { + "project": "okhttp-logging-interceptor", + "description": "Square’s meticulous HTTP client for Java and Kotlin.", + "version": "4.10.0", + "developers": [ + "Square, Inc." + ], + "url": "https://square.github.io/okhttp/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.squareup.okhttp3:logging-interceptor:4.10.0" + }, + { + "project": "okio", + "description": "A modern I/O API for Java", + "version": "3.0.0", + "developers": [ + "Square, Inc." + ], + "url": "https://github.com/square/okio/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.squareup.okio:okio-jvm:3.0.0" + }, + { + "project": "Parcelize Runtime", + "description": "Runtime library for the Parcelize compiler plugin", + "version": "1.8.10", + "developers": [ + "Kotlin Team" + ], + "url": "https://kotlinlang.org/", + "year": null, + "licenses": [ + { + "license": "The Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "org.jetbrains.kotlin:kotlin-parcelize-runtime:1.8.10" + }, + { + "project": "PdfBox-Android", + "description": "The Apache PdfBox project ported to work on Android", + "version": "2.0.27.0", + "developers": [ + "Tom Roush" + ], + "url": "https://github.com/TomRoush/PdfBox-Android", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.tom-roush:pdfbox-android:2.0.27.0" + }, + { + "project": "play-services-base", + "description": null, + "version": "18.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.gms:play-services-base:18.1.0" + }, + { + "project": "play-services-basement", + "description": null, + "version": "18.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.gms:play-services-basement:18.1.0" + }, + { + "project": "play-services-location", + "description": null, + "version": "21.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.gms:play-services-location:21.0.1" + }, + { + "project": "play-services-maps", + "description": null, + "version": "18.1.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.gms:play-services-maps:18.1.0" + }, + { + "project": "play-services-mlkit-barcode-scanning", + "description": null, + "version": "18.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0" + }, + { + "project": "play-services-tasks", + "description": null, + "version": "18.0.2", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Android Software Development Kit License", + "license_url": "https://developer.android.com/studio/terms.html" + } + ], + "dependency": "com.google.android.gms:play-services-tasks:18.0.2" + }, + { + "project": "ReLinker", + "description": "A robust native library loader for Android", + "version": "1.4.5", + "developers": [ + "KeepSafe Software, Inc." + ], + "url": "https://github.com/KeepSafe/ReLinker", + "year": "2015", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.getkeepsafe.relinker:relinker:1.4.5" + }, + { + "project": "Retrofit", + "description": "A type-safe HTTP client for Android and Java.", + "version": "2.9.0", + "developers": [ + "Square, Inc." + ], + "url": "https://github.com/square/retrofit", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.squareup.retrofit2:retrofit:2.9.0" + }, + { + "project": "Retrofit 2 Kotlin Serialization Converter", + "description": "A Converter.Factory for Kotlin\u0027s serialization support.", + "version": "0.8.0", + "developers": [ + "Jake Wharton" + ], + "url": "https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + }, + { + "project": "review", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:review:2.0.1" + }, + { + "project": "review-ktx", + "description": null, + "version": "2.0.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "Play Core Software Development Kit Terms of Service", + "license_url": "https://developer.android.com/guide/playcore/license" + } + ], + "dependency": "com.google.android.play:review-ktx:2.0.1" + }, + { + "project": "Saved State", + "description": "Android Lifecycle Saved State", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.1", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.savedstate:savedstate:1.2.1" + }, + { + "project": "SavedState Kotlin Extensions", + "description": "Kotlin extensions for \u0027savedstate\u0027 artifact", + "version": "1.2.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/savedstate#1.2.1", + "year": "2020", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.savedstate:savedstate-ktx:1.2.1" + }, + { + "project": "SLF4J API Module", + "description": "The slf4j API", + "version": "1.7.21", + "developers": [], + "url": "http://www.slf4j.org", + "year": null, + "licenses": [ + { + "license": "MIT License", + "license_url": "http://www.opensource.org/licenses/mit-license.php" + } + ], + "dependency": "org.slf4j:slf4j-api:1.7.21" + }, + { + "project": "Snapper for Jetpack Compose", + "description": "Snapper for Jetpack Compose", + "version": "0.2.2", + "developers": [ + "Chris Banes" + ], + "url": "https://github.com/chrisbanes/snapper/", + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "dev.chrisbanes.snapper:snapper:0.2.2" + }, + { + "project": "Tink Cryptography API for Android", + "description": "Tink is a small cryptographic library that provides a safe, simple, agile and fast way to accomplish some common cryptographic tasks.", + "version": "1.7.0", + "developers": [ + "" + ], + "url": "http://github.com/google/tink", + "year": null, + "licenses": [ + { + "license": "Apache License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.crypto.tink:tink-android:1.7.0" + }, + { + "project": "transport-api", + "description": null, + "version": "2.2.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.android.datatransport:transport-api:2.2.1" + }, + { + "project": "transport-backend-cct", + "description": null, + "version": "2.3.3", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.android.datatransport:transport-backend-cct:2.3.3" + }, + { + "project": "transport-runtime", + "description": null, + "version": "2.2.6", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.android.datatransport:transport-runtime:2.2.6" + }, + { + "project": "VersionedParcelable", + "description": "Provides a stable but relatively compact binary serialization format that can be passed across processes or persisted safely.", + "version": "1.1.1", + "developers": [ + "The Android Open Source Project" + ], + "url": "http://developer.android.com/tools/extras/support-library.html", + "year": "2018", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.versionedparcelable:versionedparcelable:1.1.1" + }, + { + "project": "viewbinding", + "description": null, + "version": "7.2.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.databinding:viewbinding:7.2.1" + }, + { + "project": "vision-common", + "description": null, + "version": "17.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.mlkit:vision-common:17.0.0" + }, + { + "project": "vision-interfaces", + "description": null, + "version": "16.0.0", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "ML Kit Terms of Service", + "license_url": "https://developers.google.com/ml-kit/terms" + } + ], + "dependency": "com.google.mlkit:vision-interfaces:16.0.0" + }, + { + "project": "WebView Support Library", + "description": "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions.", + "version": "1.6.0", + "developers": [ + "The Android Open Source Project" + ], + "url": "https://developer.android.com/jetpack/androidx/releases/webkit#1.6.0", + "year": "2017", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "androidx.webkit:webkit:1.6.0" + }, + { + "project": "zxcvbn4j", + "description": "This is a java port of zxcvbn, which is a JavaScript password strength generator.", + "version": "1.7.0", + "developers": [ + "Yuichi Watanabe" + ], + "url": "https://github.com/nulab/zxcvbn4j", + "year": null, + "licenses": [ + { + "license": "MIT License", + "license_url": "http://www.opensource.org/licenses/mit-license.php" + } + ], + "dependency": "com.nulab-inc:zxcvbn:1.7.0" + }, + { + "project": "ZXing Core", + "description": "Core barcode encoding/decoding library", + "version": "3.5.1", + "developers": [], + "url": null, + "year": null, + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + ], + "dependency": "com.google.zxing:core:3.5.1" + } +] \ No newline at end of file diff --git a/app/features/src/main/assets/terms_of_use.html b/app/features/src/main/assets/terms_of_use.html new file mode 100644 index 00000000..82b7e0be --- /dev/null +++ b/app/features/src/main/assets/terms_of_use.html @@ -0,0 +1,93 @@ + + + + + 2021-07-02_Nutzungsbedingungen E-Rezept-App + + + +

Nutzungsbedingungen E-Rezept-App

+ +

(Stand: Juli 2021)

+ +

Diese Bedingungen gelten für die Nutzung der E-Rezept-App der gematik GmbH.

+ +

Wer ist Anbieter der App?

+ +

Anbieter der App ist die

+ +

gematik GmbH ("gematik")
Friedrichstraße 136
10117 Berlin
+Tel. +49 30 400 41-0
Fax: +49 30 400 41-111
info@gematik.de

+ +

Um die App installieren zu können, müssen Sie ggf. zuvor bei einem App-Store-Anbieter (z.B. Apple, Google) eine Nutzungsvereinbarung über den Zugang zu dem jeweiligen App-Store abschließen. Die gematik ist nicht Vertragspartner dieser Vereinbarung.

+ +

Wie funktioniert das E-Rezept?

+ +

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

+ +

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

+ +

Der Arzt erzeugt das E-Rezept und übermittelt es direkt in die zentrale Telematikinfrastruktur. Die Telematikinfrastruktur vernetzt alle Akteure des Gesundheitswesens und gewährleistet den sektoren- und systemübergreifenden sowie sicheren Austausch von Informationen. Dort befinden sich alle entscheidenden Funktionen für die Anwendungen des digitalen Gesundheitswesens gemäß Sozialgesetzbuch Fünftes Buch, Elftes Kapitel. Das E-Rezept ist eine eigenständige Anwendung der Telematikinfrastruktur.

+ +

Sie können über die E-Rezept-App auf den Fachdienst E-Rezept zugreifen und alle elektronischen Verordnungen, die Ihnen verschiedene Ärzte ausgestellt haben, über die App abrufen. Dies geschieht entweder durch den Scan eines 2D-Codes oder durch eine Authentifizierung mit der elektronischen Gesundheitskarte. Der Arzt übermittelt die Daten der Rezepte nicht direkt an die E-Rezept-App auf Ihrem Endgerät, sondern nur in die Telematikinfrastruktur an den Fachdienst. Ein Zugriff auf die in dem Endgerät lokal gespeicherten Daten ist für den Arzt nicht möglich.

+ +

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

+ +

Allgemeines zu den Nutzungsbedingungen

+ +

Die aktuelle Version dieser Nutzungsbedingungen kann über das Menü der App abgerufen werden. Hierfür muss der Headerbereich des Homescreens durch Antippen aufgerufen und zum Impressum navigiert werden, wo die Nutzungsbedingungen hinterlegt sind.

+ +

Die gematik behält sich das Recht vor, diese Nutzungsbedingungen bei Bedarf anzupassen. Sie informiert die Nutzer über Änderungen der Nutzungsbedingungen und stellt die aktualisierte Version zur Verfügung. Ohne Zustimmung des Nutzers zu den (ggf. geänderten Nutzungsbedingungen) darf eine Nutzung der App nicht erfolgen.

+ +

Allgemeines zur App

+ +

Die App ist für die Betriebssysteme Android, Harmony und iOS erhältlich und kann in den jeweiligen App-Stores zu den dort geltenden Bedingungen unentgeltlich (in Bezug auf die Nutzung) heruntergeladen und installiert werden.

+ +

Gegebenenfalls fallen hierbei jedoch Kosten für die Datenübertragung auf das Smartphone an. +Um die Funktionen der App vollständig nutzen zu können, muss eine Internetverbindung des Smartphones, auf dem die App installiert ist, bestehen.

+ +

Die gematik ist berechtigt, die Funktionalitäten der App zu ändern oder zu erweitern oder die App selbst aus zwingenden rechtlichen oder sachlichen Gründen vorübergehend oder sogar dauerhaft einzustellen.

+ +

Die gematik wird in unterschiedlichen Intervallen Updates der App zur Verfügung stellen. Sie sollten diese Updates stets zeitnah installieren und immer die neueste verfügbare Version der App verwenden. Beim Verwenden älterer Versionen der App kann es zu Fehlfunktionen und Störungen kommen.

+ +

Aufgrund der Struktur des Internets hat die gematik keinen Einfluss auf die Datenübertragung im Internet und übernimmt deshalb keine Verantwortung für die Verfügbarkeit, Zuverlässigkeit und Qualität von Telekommunikationsnetzen, Datennetzen und technischen Einrichtungen Dritter. Leistungsstörungen auf Grund höherer Gewalt hat die gematik nicht zu vertreten.

+ +

Die gematik macht keine Zusagen über die Funktionen, Beschaffenheit, Verfügbarkeit oder Leistungsfähigkeit der App. Diese können aufgrund von Wartungsarbeiten oder Störungen vorübergehend nicht zur Verfügung stehen. In diesen Fällen kann die Funktionalität der App ganz oder teilweise eingeschränkt sein.

+ +

Nutzungsrechte

+ +

Die gematik räumt dem Nutzer das einfache, widerrufliche, nicht unterlizenzierbare, nicht übertragbare und inhaltlich auf die bestimmungsgemäße Nutzung der App (siehe Abschnitt 2 dieser Nutzungsbedingungen) beschränkte Recht ein, die App nach Maßgabe dieser Nutzungsbedingungen für eigene, nicht-kommerzielle Zwecke zu nutzen.

+ +

Es ist dementsprechend nicht gestattet, die App Dritten entgeltlich oder unentgeltlich zu überlassen, zu veröffentlichen, zu lizenzieren, zu verkaufen, anderweitig kommerziell zu verwerten oder in sonstiger Weise Dritten Rechte an der App oder ihren einzelnen Bestandteilen einzuräumen bzw. derartige Rechte zu übertragen. +Für etwaige gesonderte Leistungen Dritter, die mittels der App verfügbar gemacht werden (z.B. Botendienste von Apotheken), können separate Bedingungen der jeweiligen Dritten gelten. Die gematik ist weder für diese Leistungen noch für die entsprechenden (Nutzungs-)Bedingungen verantwortlich.

+ +

Informationen Dritter in der App

+ +

Die gematik bemüht sich um die Richtigkeit, Vollständigkeit und Aktualität der über die App zugänglich gemachten Informationen Dritter (z.B. Adressen, Öffnungszeiten, Barrierefreiheit von Zugängen etc.), übernimmt insoweit jedoch keine Gewähr.

+ +

Falls der Nutzer über Links in der App auf fremde Internetseiten gelangt, liegt die Verantwortung ebenfalls ausschließlich bei den Anbietern dieser Seiten. Die gematik macht sich die Inhalte dieser Seiten nicht zu eigen. Zum Zeitpunkt der Verlinkung waren für die gematik keine rechtswidrigen Inhalte auf den verlinkten Seiten erkennbar. Auf Änderungen der verlinkten Seiten hat die gematik keinen Einfluss.

+ +

Datenschutz

+ +

Die Datenschutzerklärung (siehe „Einstellungen/Rechtliches“) zur App gibt umfänglich Auskunft über die Verarbeitung personenbezogener Daten im Rahmen der Nutzung der App.

+ +

Schlussbestimmungen

+ +

Sollten eine oder mehrere Bestimmungen dieser Nutzungsbedingungen unwirksam, undurchführbar oder nicht durchsetzbar sein oder werden, so werden die übrigen Bestimmungen davon nicht berührt.

+ +

Es gilt das materielle Recht der Bundesrepublik Deutschland.

+ + diff --git a/android/src/main/java/android/print/PdfPrinter.kt b/app/features/src/main/kotlin/android/print/PdfPrinter.kt similarity index 82% rename from android/src/main/java/android/print/PdfPrinter.kt rename to app/features/src/main/kotlin/android/print/PdfPrinter.kt index 2925733b..f96c1d00 100644 --- a/android/src/main/java/android/print/PdfPrinter.kt +++ b/app/features/src/main/kotlin/android/print/PdfPrinter.kt @@ -3,10 +3,13 @@ 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 +/** + * Placed in "android.print" package similar to the one in internal android since + * PrintDocumentAdapter.LayoutResultCallback and PrintDocumentAdapter.WriteResultCallback + * can only be accessed from inside the android.print package + */ class PdfPrint(private val printAttributes: PrintAttributes) { suspend fun print(printAdapter: PrintDocumentAdapter, outputFd: File) = suspendCoroutine { continuation -> printAdapter.onLayout(null, printAttributes, null, object : PrintDocumentAdapter.LayoutResultCallback() { diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt new file mode 100644 index 00000000..d2d67d8d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/ErezeptApp.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app + +import android.app.Application +import de.gematik.ti.erp.app.core.AppScopedCache +import de.gematik.ti.erp.app.di.ApplicationModule + +open class ErezeptApp : Application() { + override fun onCreate() { + super.onCreate() + applicationModule = ApplicationModule(this) + } + + companion object { + val cache = AppScopedCache() + lateinit var applicationModule: ApplicationModule + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/LegalNoticeScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/LegalNoticeScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/LegalNoticeScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/LegalNoticeScreen.kt index d5bad335..4dbc4e50 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/LegalNoticeScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/LegalNoticeScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode diff --git a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt similarity index 78% rename from android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt index 634eeea2..fb7b4e9a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/MainActivity.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MainActivity.kt @@ -15,6 +15,7 @@ * limitations under the Licence. * */ +@file:Suppress("LongMethod") package de.gematik.ti.erp.app @@ -25,14 +26,11 @@ import android.nfc.Tag import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent -import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -49,29 +47,37 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.compose.rememberNavController import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE import com.google.android.play.core.install.model.UpdateAvailability import de.gematik.ti.erp.app.analytics.Analytics -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.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.MainContent -import de.gematik.ti.erp.app.mainscreen.ui.MainScreen 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.authentication.ui.ExternalAuthPrompt +import de.gematik.ti.erp.app.authentication.ui.HealthCardPrompt +import de.gematik.ti.erp.app.authentication.ui.SecureHardwarePrompt import de.gematik.ti.erp.app.cardwall.mini.ui.rememberAuthenticator +import de.gematik.ti.erp.app.cardwall.ui.ExternalAuthenticatorListViewModel +import de.gematik.ti.erp.app.core.AppContent import de.gematik.ti.erp.app.core.IntentHandler +import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.core.LocalAuthenticator import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.demomode.DemoModeActivity +import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeEnded +import de.gematik.ti.erp.app.demomode.DemoModeIntentAction.DemoModeStarted +import de.gematik.ti.erp.app.demomode.di.demoModeModule +import de.gematik.ti.erp.app.demomode.di.demoModeOverrides +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenNavigation import de.gematik.ti.erp.app.prescription.detail.ui.SharePrescriptionHandler -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler -import de.gematik.ti.erp.app.profiles.ui.rememberProfileHandler +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationModeAndMethod import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationUseCase import de.gematik.ti.erp.app.userauthentication.ui.UserAuthenticationScreen @@ -87,23 +93,24 @@ import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.android.retainedSubDI import org.kodein.di.bindProvider -import org.kodein.di.bindSingleton import org.kodein.di.compose.withDI import org.kodein.di.instance class NfcNotEnabledException : IllegalStateException() -class MainActivity : AppCompatActivity(), DIAware { - override val di by retainedSubDI(closestDI(), copy = Copy.None) { - if (BuildKonfig.INTERNAL) { - fullContainerTreeOnError = true +class MainActivity : DemoModeActivity(), DIAware { + override val di by retainedSubDI(closestDI(), copy = Copy.All) { + // should be only done from feature module + import(demoModeModule) + if (isDemoMode()) demoModeOverrides() + when { + BuildConfig.DEBUG && BuildKonfig.INTERNAL -> { + debugOverrides() + fullContainerTreeOnError = true + } } bindProvider { ExternalAuthenticatorListViewModel(instance(), instance()) } bindProvider { CheckVersionUseCase(instance(), instance()) } - - if (BuildConfig.DEBUG && BuildKonfig.INTERNAL) { - bindSingleton { TestWrapper(instance(), instance(), instance(), instance()) } - } } private val checkVersionUseCase: CheckVersionUseCase by instance() @@ -125,36 +132,46 @@ class MainActivity : AppCompatActivity(), DIAware { private val authenticationModeAndMethod: Flow get() = auth.authenticationModeAndMethod - @VisibleForTesting(otherwise = VisibleForTesting.NONE) + // @VisibleForTesting(otherwise = VisibleForTesting.NONE) // Only visible for testing, otherwise shows a warning val testWrapper: TestWrapper by instance() - @VisibleForTesting(otherwise = VisibleForTesting.NONE) + // @RestrictTo(RestrictTo.Scope.TESTS) @Stable - class Element( + class ElementForTest( val bounds: Rect, val tag: String ) - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - val elements: SnapshotStateMap = mutableStateMapOf() + // @RestrictTo(RestrictTo.Scope.TESTS) + val elementsUsedInTests: SnapshotStateMap = mutableStateMapOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - lifecycleScope.launchWhenCreated { - intent?.let { - intentHandler.propagateIntent(it) + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + intent?.let { + when (it.action) { + DemoModeStarted.name -> setAsDemoMode() + DemoModeEnded.name -> cancelDemoMode() + else -> { + cancelDemoMode() + intentHandler.propagateIntent(it) + } + } + } } } - lifecycleScope.launchWhenResumed { - checkAppUpdate() + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + checkAppUpdate() + } } if (!BuildConfig.DEBUG) { installMessageConversionExceptionHandler() } - WindowCompat.setDecorFitsSystemWindows(window, false) setContent { @@ -162,7 +179,6 @@ class MainActivity : AppCompatActivity(), DIAware { LaunchedEffect(view) { ViewCompat.setWindowInsetsAnimationCallback(view, null) } - withDI(di) { CompositionLocalProvider( LocalActivity provides this, @@ -171,8 +187,8 @@ class MainActivity : AppCompatActivity(), DIAware { LocalAuthenticator provides rememberAuthenticator(intentHandler) ) { val authenticator = LocalAuthenticator.current - - MainContent { settingsController -> + AppContent { settingsController -> + val profilesController = rememberProfilesController() val screenShotState by settingsController.screenshotState if (screenShotState.screenshotsAllowed) { @@ -194,17 +210,13 @@ class MainActivity : AppCompatActivity(), DIAware { } } val navController = rememberNavController() - val noDrawModifier = Modifier - .fillMaxSize() - .graphicsLayer(alpha = 0f) - - val profileHandler = rememberProfileHandler() - val activeProfile = profileHandler.activeProfile + val noDrawModifier = Modifier.graphicsLayer(alpha = 0f) + val activeProfile by profilesController.getActiveProfileState() val ssoTokenValid = rememberSaveable(activeProfile.ssoTokenScope) { activeProfile.ssoTokenValid() } - Box(modifier = Modifier.fillMaxSize()) { + Box { if (auth !is AuthenticationModeAndMethod.Authenticated) { Image( painterResource(R.drawable.erp_logo), @@ -231,15 +243,14 @@ class MainActivity : AppCompatActivity(), DIAware { authenticator = authenticator.authenticatorSecureElement ) - CompositionLocalProvider( - LocalProfileHandler provides rememberProfileHandler() - ) { - MainScreen( - navController = navController - ) + MainScreenNavigation( + navController = navController + ) - SharePrescriptionHandler(authenticationModeAndMethod) - } + SharePrescriptionHandler( + activeProfile = activeProfile, + authenticationModeAndMethod = authenticationModeAndMethod + ) } } @@ -255,7 +266,7 @@ class MainActivity : AppCompatActivity(), DIAware { } } if (BuildConfig.DEBUG && BuildKonfig.DEBUG_VISUAL_TEST_TAGS) { - DebugOverlay(elements) + DebugOverlay(elementsUsedInTests) } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/MessageConversionException.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/MessageConversionException.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/MessageConversionException.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt index d9d98c69..34de90fb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/Navigation.kt @@ -24,8 +24,7 @@ import android.os.Parcelable import androidx.compose.runtime.Immutable import androidx.navigation.NamedNavArgument import androidx.navigation.NavType -import de.gematik.ti.erp.app.mainscreen.ui.TaskIds -import kotlinx.serialization.decodeFromString +import de.gematik.ti.erp.app.mainscreen.navigation.TaskIds import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json diff --git a/android/src/main/java/de/gematik/ti/erp/app/TestTags.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/TestTags.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/TestTags.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/TestWrapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/TestWrapper.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt index b7ea67ec..b44f0d0c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/TestWrapper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/TestWrapper.kt @@ -18,9 +18,10 @@ package de.gematik.ti.erp.app +import de.gematik.ti.erp.app.features.BuildConfig import de.gematik.ti.erp.app.idp.usecase.IdpUseCase -import de.gematik.ti.erp.app.prescription.repository.LocalDataSource -import de.gematik.ti.erp.app.prescription.repository.RemoteDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase import io.github.aakira.napier.Napier import kotlinx.coroutines.Dispatchers @@ -30,6 +31,9 @@ import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.spec.ECPrivateKeySpec import org.bouncycastle.util.encoders.Base64 import org.jose4j.jws.EcdsaUsingShaAlgorithm +import org.kodein.di.DI +import org.kodein.di.bindSingleton +import org.kodein.di.instance import java.math.BigInteger import java.security.KeyFactory import java.security.Signature @@ -38,8 +42,8 @@ private const val SignatureOutputSize = 64 class TestWrapper( private val profilesUseCase: ProfilesUseCase, - private val remoteDataSource: RemoteDataSource, - private val localDataSource: LocalDataSource, + private val remoteDataSource: PrescriptionRemoteDataSource, + private val localDataSource: PrescriptionLocalDataSource, private val idpUseCase: IdpUseCase ) { init { @@ -89,3 +93,7 @@ class TestWrapper( } } } + +fun DI.MainBuilder.debugOverrides() { + bindSingleton { TestWrapper(instance(), instance(), instance(), instance()) } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/VisibleDebugTree.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/VisibleDebugTree.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt similarity index 91% rename from android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt index 4ab69cd1..3c772ddf 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/analytics/Analytics.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/Analytics.kt @@ -24,9 +24,9 @@ import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.content.edit +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.contentsquare.android.Contentsquare import de.gematik.ti.erp.app.Requirement @@ -37,9 +37,10 @@ import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.mainscreen.ui.MainScreenBottomSheetContentState import de.gematik.ti.erp.app.pharmacy.ui.PharmacySearchSheetContentState import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailBottomSheetContent +import io.github.aakira.napier.Napier +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.combine private const val PrefsName = "analyticsAllowed" @@ -102,7 +103,7 @@ class Analytics( val screenState @Composable - get() = analyticsScreenFlow.collectAsState(AnalyticsData.defaultAnalyticsState) + get() = analyticsScreenFlow.collectAsStateWithLifecycle(AnalyticsData.defaultAnalyticsState) fun onPopUpShown(popUpScreenName: String) { if (analyticsAllowed.value) { @@ -185,8 +186,9 @@ class Analytics( } } +@Suppress("ComposableNaming") @Composable -fun TrackNavigationChanges( +fun trackNavigationChanges( navController: NavHostController, previousNavEntry: String, onNavEntryChange: (String) -> Unit @@ -195,15 +197,17 @@ fun TrackNavigationChanges( val analyticsState by analytics.screenState LaunchedEffect(navController.currentBackStackEntry) { - try { - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - if (route != previousNavEntry) { - onNavEntryChange(route) - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + async { + try { + val route = Uri.parse(navController.currentBackStackEntry?.destination?.route) + .buildUpon().clearQuery().build().toString() + if (route != previousNavEntry) { + onNavEntryChange(route) + trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + } + } catch (expected: Exception) { + Napier.e("Couldn't track navigation screen", expected) } - } catch (expected: Exception) { - Napier.e("Couldn't track navigation screen", expected) } } } @@ -215,18 +219,20 @@ fun trackScreenUsingNavEntry( ) { try { val name = analyticsList.find { it.key == route }?.name ?: "" - if (name.isNotEmpty()) { - analytics.trackScreen(name) - } else { - analytics.trackScreen(route) + val trackedName = when { + name.isNotEmpty() -> name + else -> route } + Napier.d { "Content square tracking: Key is $trackedName" } + analytics.trackScreen(trackedName) } catch (expected: Exception) { Napier.e("Couldn't track navigation screen", expected) } } +@Suppress("ComposableNaming") @Composable -fun TrackPopUps( +fun trackPopUps( analytics: Analytics, analyticsState: AnalyticsData.AnalyticsScreenState ) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt index 600feccb..5d0659c2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/analytics/usecase/AnalyticsUseCase.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.analytics.usecase import android.content.Context import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.serialization.Serializable diff --git a/android/src/main/java/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/attestation/usecase/IntegrityUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ProfilesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt similarity index 52% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ProfilesModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt index 58dafe25..e4bd34c3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ProfilesModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/di/authenticationModule.kt @@ -16,21 +16,13 @@ * */ -package de.gematik.ti.erp.app.profiles +package de.gematik.ti.erp.app.authentication.di -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository -import de.gematik.ti.erp.app.profiles.usecase.ProfileAvatarUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase -import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase +import de.gematik.ti.erp.app.authentication.mapper.DefaultPromptAuthenticationProvider +import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider import org.kodein.di.DI import org.kodein.di.bindProvider -import org.kodein.di.bindSingleton -import org.kodein.di.instance -val profilesModule = DI.Module("profilesModule") { - bindProvider { ProfileAvatarUseCase(instance(), instance()) } - bindProvider { ProfilesWithPairedDevicesUseCase(instance(), instance()) } - - bindSingleton { ProfilesRepository(instance(), instance()) } - bindSingleton { ProfilesUseCase(instance(), instance()) } +val authenticationModule = DI.Module("authenticationModule", allowSilentOverride = true) { + bindProvider { DefaultPromptAuthenticationProvider() } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt new file mode 100644 index 00000000..4740db77 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/mapper/DefaultPromptAuthenticationProvider.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.mapper + +import de.gematik.ti.erp.app.authentication.model.External +import de.gematik.ti.erp.app.authentication.model.HealthCard +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.None +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.authentication.model.SecureElement +import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalPromptAuthenticator +import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPromptAuthenticator +import de.gematik.ti.erp.app.cardwall.mini.ui.SecureHardwarePromptAuthenticator +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DefaultPromptAuthenticationProvider : PromptAuthenticationProvider { + override fun mapAuthenticationResult( + id: ProfileIdentifier, + initialAuthenticationData: InitialAuthenticationData, + scope: PromptAuthenticator.AuthScope, + authenticators: List + ): Flow { + val healthCardPrompt = authenticators + .filterIsInstance().first() + + val securedHardwarePrompt = authenticators + .filterIsInstance().first() + + val externalPrompt = authenticators + .filterIsInstance().first() + + return when (initialAuthenticationData) { + is External -> externalPrompt.authenticate(id, scope) + is HealthCard -> healthCardPrompt.authenticate(id, scope) + is SecureElement -> securedHardwarePrompt.authenticate(id, scope) + is None -> flowOf(PromptAuthenticator.AuthResult.NoneEnrolled) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt similarity index 54% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt index 4f29f686..ef67f4d8 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/ExternalAuthPrompt.kt @@ -16,25 +16,22 @@ * */ -package de.gematik.ti.erp.app.cardwall.mini.ui +package de.gematik.ti.erp.app.authentication.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,119 +40,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.systemBarsPadding -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.core.IntentHandler +import de.gematik.ti.erp.app.cardwall.mini.ui.ExternalPromptAuthenticator +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.mainscreen.ui.ExternalAuthenticationDialog -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.Dialog import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import io.github.aakira.napier.Napier -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -@Stable -class ExternalPromptAuthenticator( - private val intentHandler: IntentHandler, - private val bridge: AuthenticationBridge -) : PromptAuthenticator { - private sealed interface Request { - object InsuranceSelected : Request - object Cancel : Request - } - - private val requestChannel = Channel(Channel.RENDEZVOUS) - - @Stable - internal sealed interface State { - object None : State - data class SelectInsurance(val authenticatorName: String) : State - } - - internal var state by mutableStateOf(State.None) - - var profile by mutableStateOf(null) - private set - - var isInProgress: Boolean = false - private set - - override fun authenticate( - profileId: ProfileIdentifier, - scope: PromptAuthenticator.AuthScope - ): Flow = channelFlow { - when (val authFor = bridge.authenticateFor(profileId)) { - is AuthenticationBridge.External -> { - state = State.SelectInsurance(authFor.authenticatorName) - profile = authFor.profile - - requestChannel.receiveAsFlow().collectLatest { - when (it) { - Request.Cancel -> { - send(PromptAuthenticator.AuthResult.Cancelled) - cancel() - } - - is Request.InsuranceSelected -> { - Napier.d("Fasttrack: doExternalAuthentication for $authFor") - - bridge.doExternalAuthentication( - profileId = profileId, - scope = scope, - authenticatorId = authFor.authenticatorId, - authenticatorName = authFor.authenticatorName - ).onSuccess { redirect -> - intentHandler.startFastTrackApp(redirect) - }.onFailure { - Napier.e("doExternalAuthentication failed", it) - // TODO error handling - send(PromptAuthenticator.AuthResult.Cancelled) - cancel() - } - - Napier.d("Fasttrack: wait for instant of $authFor") - } - } - } - } - - else -> { - send(PromptAuthenticator.AuthResult.Cancelled) - } - } - }.onStart { - isInProgress = true - }.onCompletion { - isInProgress = false - state = State.None - profile = null - } - - internal suspend fun onInsuranceSelected() { - requestChannel.send(Request.InsuranceSelected) - } - - internal suspend fun onCancel() { - requestChannel.send(Request.Cancel) - } - - override suspend fun cancelAuthentication() { - requestChannel.send(Request.Cancel) - } -} - @Composable fun ExternalAuthPrompt( authenticator: ExternalPromptAuthenticator @@ -224,13 +118,3 @@ fun ExternalAuthPrompt( ExternalAuthenticationDialog() } } - -@Composable -fun rememberExternalPromptAuthenticator( - bridge: AuthenticationBridge, - intentHandler: IntentHandler -): ExternalPromptAuthenticator { - return remember { - ExternalPromptAuthenticator(intentHandler, bridge) - } -} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt new file mode 100644 index 00000000..d98aa74f --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardCredentials.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconToggleButton +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.PrimaryButton + +private val PinRegex = """^\d{0,8}$""".toRegex() +private val PinCorrectRegex = """^\d{6,8}$""".toRegex() + +@Composable +internal fun HealthCardCredentials( + modifier: Modifier, + onNext: (pin: String) -> Unit +) { + var pin by remember { mutableStateOf("") } + var pinVisible by remember { mutableStateOf(false) } + val pinCorrect by remember { + derivedStateOf { pin.matches(PinCorrectRegex) } + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Large) + ) { + Text( + stringResource(R.string.mini_cdw_intro_description), + style = AppTheme.typography.body2l + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = pin, + onValueChange = { + if (it.matches(PinRegex)) { + pin = it + } + }, + label = { Text(stringResource(R.string.mini_cdw_pin_input_label)) }, + placeholder = { Text(stringResource(R.string.mini_cdw_pin_input_placeholder)) }, + visualTransformation = if (pinVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.NumberPassword + ), + shape = RoundedCornerShape(8.dp), + colors = TextFieldDefaults.outlinedTextFieldColors( + unfocusedLabelColor = AppTheme.colors.neutral400, + placeholderColor = AppTheme.colors.neutral400, + trailingIconColor = AppTheme.colors.neutral400 + ), + keyboardActions = KeyboardActions { + onNext(pin) + }, + trailingIcon = { + IconToggleButton( + checked = pinVisible, + onCheckedChange = { pinVisible = it } + ) { + Icon( + if (pinVisible) { + Icons.Rounded.Visibility + } else { + Icons.Rounded.VisibilityOff + }, + null + ) + } + } + ) + PrimaryButton( + onClick = { onNext(pin) }, + enabled = pinCorrect, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.mini_cdw_pin_next)) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt new file mode 100644 index 00000000..07989c43 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardErrorDialog.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPromptAuthenticator +import de.gematik.ti.erp.app.cardwall.ui.pinRetriesLeft +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.AcceptDialog +import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString + +@Requirement( + "A_20079", + "A_20085", + "A_20605#2", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Display error messages from endpoint." +) +@Composable +internal fun HealthCardErrorDialog( + state: HealthCardPromptAuthenticator.State.ReadState.Error, + onCancel: () -> Unit, + onEnableNfc: () -> Unit +) { + if (state == HealthCardPromptAuthenticator.State.ReadState.Error.NfcDisabled) { + CommonAlertDialog( + header = stringResource(R.string.cdw_enable_nfc_header), + info = stringResource(R.string.cdw_enable_nfc_info), + cancelText = stringResource(R.string.cancel), + actionText = stringResource(R.string.cdw_enable_nfc_btn_text), + onCancel = onCancel, + onClickAction = onEnableNfc + ) + } else { + val retryText = when (state) { + HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationFailed -> Pair( + stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), + stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() + ) + + HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationInvalidCertificate -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() + ) + + HealthCardPromptAuthenticator.State.ReadState.Error.RemoteCommunicationInvalidOCSP -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString() + ) + + HealthCardPromptAuthenticator.State.ReadState.Error.CardAccessNumberWrong -> Pair( + stringResource(R.string.cdw_nfc_intro_step2_header_on_can_error_alert).toAnnotatedString(), + stringResource(R.string.cdw_nfc_intro_step2_info_on_can_error).toAnnotatedString() + ) + + is HealthCardPromptAuthenticator.State.ReadState.Error.PersonalIdentificationWrong -> Pair( + stringResource(R.string.cdw_nfc_intro_step2_header_on_pin_error_alert).toAnnotatedString(), + pinRetriesLeft(state.retriesLeft) + ) + + HealthCardPromptAuthenticator.State.ReadState.Error.HealthCardBlocked -> Pair( + stringResource(R.string.cdw_header_on_card_blocked).toAnnotatedString(), + stringResource(R.string.cdw_info_on_card_blocked).toAnnotatedString() + ) + + else -> null + } + + retryText?.let { (title, message) -> + + AcceptDialog( + header = title, + info = message, + acceptText = stringResource(R.string.ok), + onClickAccept = onCancel + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt new file mode 100644 index 00000000..b83de41a --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/HealthCardPrompt.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.ui + +import android.content.Intent +import android.provider.Settings +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import de.gematik.ti.erp.app.cardwall.mini.ui.HealthCardPromptAuthenticator +import de.gematik.ti.erp.app.cardwall.ui.ReadingCardAnimation +import de.gematik.ti.erp.app.cardwall.ui.SearchingCardAnimation +import de.gematik.ti.erp.app.cardwall.ui.TagLostCard +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.Dialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private const val InfoTextRoundTime = 5000L + +@Composable +fun HealthCardPrompt( + authenticator: HealthCardPromptAuthenticator +) { + val scope = rememberCoroutineScope() + val state = authenticator.state + val profile = authenticator.profile + + val isError = state is HealthCardPromptAuthenticator.State.ReadState.Error + val isTagLost = state is HealthCardPromptAuthenticator.State.ReadState.Error.TagLost + + if (state != HealthCardPromptAuthenticator.State.None && (!isError || isTagLost)) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) + ) { + Box( + Modifier + .semantics(false) { } + .fillMaxSize() + .background(SolidColor(Color.Black), alpha = 0.5f) + .verticalScroll(rememberScrollState()) + .imePadding() + .systemBarsPadding(), + contentAlignment = Alignment.BottomCenter + ) { + PromptScaffold( + title = stringResource(R.string.mini_cdw_title), + profile = profile, + onCancel = { + scope.launch { + authenticator.onCancel() + } + } + ) { + when (state) { + HealthCardPromptAuthenticator.State.EnterCredentials -> + HealthCardCredentials( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + onNext = { + scope.launch { + authenticator.onCredentialsEntered(it) + } + } + ) + + is HealthCardPromptAuthenticator.State.ReadState -> + HealthCardAnimation( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + state = state + ) + + else -> {} + } + } + } + } + } + if (isError) { + HealthCardErrorDialog( + state = state as HealthCardPromptAuthenticator.State.ReadState.Error, + onCancel = { + scope.launch { + authenticator.onCancel() + } + }, + onEnableNfc = { + scope.launch(Dispatchers.Main) { + authenticator.activity.startActivity( + Intent(Settings.ACTION_NFC_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + authenticator.onCancel() + } + } + ) + } +} + +@Composable +private fun HealthCardAnimation( + modifier: Modifier, + state: HealthCardPromptAuthenticator.State.ReadState +) { + Column( + modifier = modifier + .padding(PaddingDefaults.Large) + .wrapContentSize() + .testTag("cdw_auth_nfc_bottom_sheet"), + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .defaultMinSize(minHeight = 150.dp) + .fillMaxWidth() + ) { + when (state) { + HealthCardPromptAuthenticator.State.ReadState.Searching -> SearchingCardAnimation() + is HealthCardPromptAuthenticator.State.ReadState.Reading -> ReadingCardAnimation() + is HealthCardPromptAuthenticator.State.ReadState.Error -> TagLostCard() + } + } + + // how to hold your card + val rotatingScanCardAssistance = listOf( + Pair( + stringResource(R.string.cdw_nfc_search1_headline), + stringResource(R.string.cdw_nfc_search1_info) + ), + Pair( + stringResource(R.string.cdw_nfc_search2_headline), + stringResource(R.string.cdw_nfc_search2_info) + ), + Pair( + stringResource(R.string.cdw_nfc_search3_headline), + stringResource(R.string.cdw_nfc_search3_info) + ) + ) + + var info by remember { mutableStateOf(rotatingScanCardAssistance.first()) } + + LaunchedEffect(Unit) { + while (true) { + snapshotFlow { state } + .first { + state is HealthCardPromptAuthenticator.State.ReadState.Searching + } + + var i = 0 + while (state is HealthCardPromptAuthenticator.State.ReadState.Searching) { + info = rotatingScanCardAssistance[i] + + i = if (i < rotatingScanCardAssistance.size - 1) { + i + 1 + } else { + 0 + } + + delay(InfoTextRoundTime) + } + } + } + + info = when (state) { + HealthCardPromptAuthenticator.State.ReadState.Reading.Reading00 -> Pair( + stringResource(R.string.cdw_nfc_found_headline), + stringResource(R.string.cdw_nfc_found_info) + ) + + HealthCardPromptAuthenticator.State.ReadState.Reading.Reading25 -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_trusted_channel_established), + stringResource(R.string.cdw_nfc_communication_info) + ) + + HealthCardPromptAuthenticator.State.ReadState.Reading.Reading50 -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_certificate_loaded), + stringResource(R.string.cdw_nfc_communication_info) + ) + + HealthCardPromptAuthenticator.State.ReadState.Reading.Reading75 -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_pin_verified), + stringResource(R.string.cdw_nfc_communication_info) + ) + + HealthCardPromptAuthenticator.State.ReadState.Reading.Success -> Pair( + stringResource(R.string.cdw_nfc_communication_headline_challenge_signed), + stringResource(R.string.cdw_nfc_communication_info) + ) + + HealthCardPromptAuthenticator.State.ReadState.Error.TagLost -> Pair( + stringResource(R.string.cdw_nfc_tag_lost_headline), + stringResource(R.string.cdw_nfc_tag_lost_info) + ) + + else -> info + } + + Text( + info.first, + style = AppTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Text( + info.second, + style = AppTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt new file mode 100644 index 00000000..9ce8e229 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/PromptScaffold.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.ui + +import androidx.compose.foundation.layout.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.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.ui.Avatar +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.SpacerMedium + +@Composable +fun PromptScaffold( + title: String, + profile: ProfilesUseCaseData.Profile?, + onCancel: () -> Unit, + content: @Composable () -> Unit +) { + Surface( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + color = MaterialTheme.colors.surface, + shape = RoundedCornerShape(16.dp), + elevation = 8.dp + ) { + Column( + Modifier + .padding(vertical = PaddingDefaults.Medium) + ) { + Row( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + profile?.let { + Avatar( + modifier = Modifier.size(36.dp), + emptyIcon = Icons.Rounded.PersonOutline, + iconModifier = Modifier.size(20.dp), + profile = profile, + ssoStatusColor = null + ) + SpacerMedium() + Column(modifier = Modifier.weight(1f)) { + Text( + title, + style = AppTheme.typography.h6, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Text( + profile.insurance.insuranceIdentifier, + style = AppTheme.typography.body2l + ) + } + } + TextButton(onClick = onCancel) { + Text(stringResource(R.string.cdw_nfc_dlg_cancel)) + } + } + SpacerLarge() + content() + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt new file mode 100644 index 00000000..55e10aa5 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/authentication/ui/SecureHardwarePrompt.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.cardwall.mini.ui.SecureHardwarePromptAuthenticator +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.utils.compose.AcceptDialog +import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import kotlinx.coroutines.launch + +@Composable +fun SecureHardwarePrompt( + authenticator: SecureHardwarePromptAuthenticator +) { + val scope = rememberCoroutineScope() + authenticator.showError?.let { error -> + val retryText = when (error) { + is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful -> Pair( + stringResource(R.string.cdw_mini_alt_auth_removed_title).toAnnotatedString(), + stringResource(R.string.cdw_mini_alt_auth_removed).toAnnotatedString() + ) + + SecureHardwarePromptAuthenticator.Error.RemoteCommunicationFailed -> Pair( + stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), + stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() + ) + + SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidCertificate -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() + ) + + SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidOCSP -> Pair( + stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString(), + stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) + .toAnnotatedString() + ) + } + + retryText.let { (title, message) -> + AcceptDialog( + header = title, + info = message, + acceptText = stringResource(R.string.ok), + onClickAccept = { + scope.launch { + if (error is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful) { + authenticator.removeAuthentication(error.profileId) + } + authenticator.resetErrorState() + } + } + ) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/CardUnlockModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/CardUnlockModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/CardUnlockModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/CardUnlockModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/model/UnlockEgkNavigation.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt index 72d2b482..b6421f0a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgKComponents.kt @@ -44,9 +44,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.model.UnlockEgkNavigation import de.gematik.ti.erp.app.cardwall.ui.CardAccessNumber @@ -55,10 +54,12 @@ 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.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.troubleshooting.TroubleShootingScreen import de.gematik.ti.erp.app.utils.compose.HintCard import de.gematik.ti.erp.app.utils.compose.HintSmallImage import de.gematik.ti.erp.app.utils.compose.NavigationAnimation @@ -70,6 +71,7 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import org.kodein.di.compose.rememberInstance const val SECRET_MIN_LENGTH = 6 const val SECRET_MAX_LENGTH = 8 @@ -80,18 +82,23 @@ sealed class ToggleUnlock { data class ToggleByHealthCard(val tag: Tag) : ToggleUnlock() } +// TODO: Make method smaller +@Suppress("LongMethod") @Composable fun UnlockEgKScreen( unlockMethod: UnlockMethod, onCancel: () -> Unit, onClickLearnMore: () -> Unit ) { + val buildConfig by rememberInstance() val unlockEgkController = rememberUnlockEgkController() var unlockMethod by rememberSaveable { mutableStateOf(unlockMethod) } val unlockNavController = rememberNavController() var previousNavEntry by remember { mutableStateOf("healthCardPassword_introduction") } - TrackNavigationChanges(unlockNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + + trackNavigationChanges(unlockNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + var cardAccessNumber by rememberSaveable { mutableStateOf("") } var personalUnblockingKey by rememberSaveable { mutableStateOf("") } var oldSecret by rememberSaveable { mutableStateOf("") } @@ -177,6 +184,7 @@ fun UnlockEgKScreen( composable(UnlockEgkNavigation.UnlockEgk.route) { NavigationAnimation { UnlockScreen( + buildConfig = buildConfig, unlockMethod = unlockMethod, unlockEgkController = unlockEgkController, cardAccessNumber = cardAccessNumber, @@ -588,13 +596,15 @@ private fun UnlockScreen( onRetryPuk: () -> Unit, onBack: () -> Unit, onFinishUnlock: () -> Unit, - onAssignPin: () -> Unit + onAssignPin: () -> Unit, + buildConfig: BuildConfigInformation ) { val nfcPositionState = rememberCardWallNfcPositionState() val state = nfcPositionState.state val dialogState = rememberUnlockEgkDialogState() UnlockEgkDialog( + buildConfig = buildConfig, unlockMethod = unlockMethod, dialogState = dialogState, unlockEgkController = unlockEgkController, diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt index 35c90aeb..37b39a86 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkController.kt @@ -53,7 +53,7 @@ class UnlockEgkController( oldSecret = oldSecret, newSecret = newSecret, cardChannel = cardChannel - ).flowOn(dispatchers.IO) + ).flowOn(dispatchers.io) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt index 5992dc20..47fb011e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/ui/UnlockEgkDialog.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -45,6 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -54,11 +56,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.ui.platform.LocalContext import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.NfcNotEnabledException -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.usecase.UnlockEgkState import de.gematik.ti.erp.app.cardwall.ui.CardAnimationBox @@ -68,6 +67,8 @@ import de.gematik.ti.erp.app.cardwall.ui.InfoText import de.gematik.ti.erp.app.cardwall.ui.pinRetriesLeft import de.gematik.ti.erp.app.cardwall.ui.rotatingScanCardAssistance import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation import de.gematik.ti.erp.app.settings.ui.buildFeedbackBodyWithDeviceInfo import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -75,6 +76,7 @@ import de.gematik.ti.erp.app.utils.compose.AcceptDialog import de.gematik.ti.erp.app.utils.compose.Dialog import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import io.github.aakira.napier.Napier import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -84,10 +86,9 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.retryWhen import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean @@ -116,6 +117,7 @@ fun UnlockEgkDialog( personalUnblockingKey: String, oldSecret: String, newSecret: String, + buildConfig: BuildConfigInformation, onClickTroubleshooting: (() -> Unit)? = null, troubleShootingEnabled: Boolean = false, onRetryCan: () -> Unit, @@ -251,6 +253,7 @@ fun UnlockEgkDialog( resumeText = it, onFinishUnlock = onFinishUnlock, nextText = nextText, + buildConfig = buildConfig, onToggleUnlock = { coroutineScope.launch { toggleUnlock.emit(ToggleUnlock.ToggleByUser(it)) @@ -321,6 +324,7 @@ private fun resumeTextFromUnlockEgkState( stringResource(R.string.unlock_egk_wrong_pin).toAnnotatedString(), pinRetriesLeft(state.retriesLeft) ) + UnlockEgkState.HealthCardPasswordBlocked -> Pair( stringResource(R.string.unlock_egk_password_blocked).toAnnotatedString(), stringResource(R.string.unlock_egk_password_blocked_info).toAnnotatedString() @@ -367,13 +371,18 @@ private fun ResumeDialog( onRetryCan: () -> Unit, onRetryOldSecret: () -> Unit, onRetryPuk: () -> Unit, - onAssignPin: () -> Unit + onAssignPin: () -> Unit, + buildConfig: BuildConfigInformation ) { val context = LocalContext.current val mailAddress = stringResource(R.string.settings_contact_mail_address) val subject = stringResource(R.string.settings_feedback_mail_subject) val body = buildFeedbackBodyWithDeviceInfo( - context = context, + darkMode = buildConfig.inDarkTheme(), + language = buildConfig.language(), + versionName = buildConfig.versionName(), + nfcInfo = buildConfig.nfcInformation(context), + phoneModel = buildConfig.model(), errorState = remember(key1 = state.name) { state.name } ) @@ -405,6 +414,7 @@ private fun ResumeDialog( UnlockEgkState.PasswordNotFound, UnlockEgkState.SecurityStatusNotSatisfied, UnlockEgkState.MemoryFailure -> openMailClient(context, mailAddress, body, subject) + UnlockEgkState.PasswordNotUsable -> onAssignPin() // retry else -> onToggleUnlock(true) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardunlock/usecase/UnlockEgkUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/CardWallModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/CardWallModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/CardWallModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/domain/biometric/Biometric.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt new file mode 100644 index 00000000..1680788e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/Authentication.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.cardwall.mini.ui + +import android.nfc.Tag +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import de.gematik.ti.erp.app.authentication.mapper.PromptAuthenticationProvider +import de.gematik.ti.erp.app.authentication.model.External +import de.gematik.ti.erp.app.authentication.model.HealthCard +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.None +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.authentication.model.SecureElement +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState +import de.gematik.ti.erp.app.core.IntentHandler +import de.gematik.ti.erp.app.idp.api.models.AuthenticationId +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import org.kodein.di.compose.rememberInstance +import java.net.URI + +class NoneEnrolledException : IllegalStateException() +class UserNotAuthenticatedException : IllegalStateException() + +interface AuthenticationBridge { + suspend fun authenticateFor( + profileId: ProfileIdentifier + ): InitialAuthenticationData + + fun doSecureElementAuthentication( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope + ): Flow + + fun doHealthCardAuthentication( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope, + can: String, + pin: String, + tag: Tag + ): Flow + + suspend fun loadExternalAuthenticators(): List + + suspend fun doExternalAuthentication( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope, + authenticatorId: String, + authenticatorName: String + ): Result + + suspend fun doExternalAuthorization( + redirect: URI + ): Result + + suspend fun doRemoveAuthentication(profileId: ProfileIdentifier) +} + +/** + * TODO: Modify implementation + * Implementation does not follow a particular design pattern by + * having controllers calling controllers and having composable in the same file as + * the controllers and having the whole feature hidden inside the cardwall.ui + */ +@Stable +class Authenticator( + val authenticatorSecureElement: SecureHardwarePromptAuthenticator, + val authenticatorHealthCard: HealthCardPromptAuthenticator, + val authenticatorExternal: ExternalPromptAuthenticator, + val mapper: PromptAuthenticationProvider, + private val bridge: AuthenticationBridge +) { + fun authenticateForPrescriptions(profileId: ProfileIdentifier): Flow = + flow { + val initialAuthenticationData = bridge.authenticateFor(profileId) + emitAll( + mapper.mapAuthenticationResult( + id = profileId, + initialAuthenticationData = initialAuthenticationData, + scope = PromptAuthenticator.AuthScope.Prescriptions, + authenticators = listOf( + authenticatorHealthCard, + authenticatorSecureElement, + authenticatorExternal + ) + ) + ) + } + + fun authenticateForPairedDevices(profileId: ProfileIdentifier): Flow = + flow { + emitAll( + when (bridge.authenticateFor(profileId)) { + is HealthCard -> + authenticatorHealthCard.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) + + is SecureElement -> + authenticatorSecureElement.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) + + is External -> + authenticatorExternal.authenticate(profileId, PromptAuthenticator.AuthScope.PairedDevices) + + is None -> flowOf(PromptAuthenticator.AuthResult.NoneEnrolled) + } + ) + } + + suspend fun cancelAllAuthentications() { + authenticatorSecureElement.cancelAuthentication() + authenticatorHealthCard.cancelAuthentication() + } +} + +@Composable +fun rememberAuthenticator(intentHandler: IntentHandler): Authenticator { + val bridge = rememberMiniCardWallController() + val promptSE = rememberSecureHardwarePromptAuthenticator(bridge) + val promptHC = rememberHealthCardPromptAuthenticator(bridge) + val promptEX = rememberExternalPromptAuthenticator(bridge, intentHandler) + val mapper by rememberInstance() + return remember { + Authenticator( + authenticatorSecureElement = promptSE, + authenticatorHealthCard = promptHC, + authenticatorExternal = promptEX, + bridge = bridge, + mapper = mapper + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt new file mode 100644 index 00000000..62bd891e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/ExternalAuthPrompt.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.cardwall.mini.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import de.gematik.ti.erp.app.authentication.model.External +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.core.IntentHandler +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import io.github.aakira.napier.Napier +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow + +@Stable +class ExternalPromptAuthenticator( + private val intentHandler: IntentHandler, + private val bridge: AuthenticationBridge +) : PromptAuthenticator { + private sealed interface Request { + object InsuranceSelected : Request + object Cancel : Request + } + + private val requestChannel = Channel(Channel.RENDEZVOUS) + + @Stable + internal sealed interface State { + object None : State + data class SelectInsurance(val authenticatorName: String) : State + } + + internal var state by mutableStateOf(State.None) + + var profile by mutableStateOf(null) + private set + + var isInProgress: Boolean = false + private set + + override fun authenticate( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope + ): Flow = channelFlow { + when (val authFor = bridge.authenticateFor(profileId)) { + is External -> { + state = State.SelectInsurance(authFor.authenticatorName) + profile = authFor.profile + + requestChannel.receiveAsFlow().collectLatest { + when (it) { + Request.Cancel -> { + send(PromptAuthenticator.AuthResult.Cancelled) + cancel() + } + + is Request.InsuranceSelected -> { + Napier.d("Fasttrack: doExternalAuthentication for $authFor") + + bridge.doExternalAuthentication( + profileId = profileId, + scope = scope, + authenticatorId = authFor.authenticatorId, + authenticatorName = authFor.authenticatorName + ).onSuccess { redirect -> + intentHandler.startFastTrackApp(redirect) + }.onFailure { + Napier.e("doExternalAuthentication failed", it) + // TODO error handling + send(PromptAuthenticator.AuthResult.Cancelled) + cancel() + } + + Napier.d("Fasttrack: wait for instant of $authFor") + } + } + } + } + + else -> { + send(PromptAuthenticator.AuthResult.Cancelled) + } + } + }.onStart { + isInProgress = true + }.onCompletion { + isInProgress = false + state = State.None + profile = null + } + + internal suspend fun onInsuranceSelected() { + requestChannel.send(Request.InsuranceSelected) + } + + internal suspend fun onCancel() { + requestChannel.send(Request.Cancel) + } + + override suspend fun cancelAuthentication() { + requestChannel.send(Request.Cancel) + } +} + +@Composable +fun rememberExternalPromptAuthenticator( + bridge: AuthenticationBridge, + intentHandler: IntentHandler +): ExternalPromptAuthenticator { + return remember { + ExternalPromptAuthenticator(intentHandler, bridge) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt new file mode 100644 index 00000000..e748e374 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/HealthCardPrompt.kt @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.cardwall.mini.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import de.gematik.ti.erp.app.MainActivity +import de.gematik.ti.erp.app.NfcNotEnabledException +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.authentication.model.HealthCard +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.receiveAsFlow + +@Stable +class HealthCardPromptAuthenticator( + val activity: MainActivity, + private val bridge: AuthenticationBridge +) : PromptAuthenticator { + private sealed interface Request { + class CredentialsEntered(val pin: String) : Request + object Cancel : Request + } + + private val requestChannel = Channel(Channel.RENDEZVOUS) + + internal sealed interface State { + object None : State + object EnterCredentials : State + + sealed interface ReadState : State { + object Searching : ReadState + + sealed interface Reading : ReadState { + object Reading00 : Reading + object Reading25 : Reading + object Reading50 : Reading + object Reading75 : Reading + object Success : Reading + } + + sealed interface Error : ReadState { + object NfcDisabled : Error + object TagLost : Error + object RemoteCommunicationFailed : Error + object CardAccessNumberWrong : Error + class PersonalIdentificationWrong(val retriesLeft: Int) : Error + object HealthCardBlocked : Error + object RemoteCommunicationInvalidCertificate : Error + object RemoteCommunicationInvalidOCSP : Error + } + } + } + + internal var state by mutableStateOf(State.None) + private set + + var profile by mutableStateOf(null) + private set + + private val tagFlow = activity.nfcTagFlow + .filter { + // only let interrupted communications through + !(state is State.ReadState.Error && state!=State.ReadState.Error.TagLost) + } + + override fun authenticate( + profileId: ProfileIdentifier, + scope: PromptAuthenticator.AuthScope + ): Flow = channelFlow { + val requestChannelFlow = requestChannel.receiveAsFlow() + + when (val authFor = bridge.authenticateFor(profileId)) { + is HealthCard -> { + state = State.EnterCredentials + profile = authFor.profile + + requestChannelFlow.collectLatest { req -> + when (req) { + Request.Cancel -> { + send(PromptAuthenticator.AuthResult.Cancelled) + cancel() + } + + is Request.CredentialsEntered -> { + state = State.ReadState.Searching + + tagFlow + .catch { + if (it is NfcNotEnabledException) { + state = State.ReadState.Error.NfcDisabled + } + } + .collectLatest { tag -> + bridge.doHealthCardAuthentication( + profileId = profileId, + scope = scope, + can = authFor.can, + pin = req.pin, + tag = tag + ).collect { + it.emitAuthState() + if (it.isFinal()) { + send(PromptAuthenticator.AuthResult.Authenticated) + cancel() + } + } + } + } + } + } + } + + else -> { + send(PromptAuthenticator.AuthResult.Cancelled) + } + } + }.onCompletion { + state = State.None + profile = null + } + + @Requirement( + "A_19937", + "A_20079", + "A_20605#1", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Propagates IDP auth states to the user." + ) + @Requirement( + "GS-A_5542#1", + sourceSpecification = "gemSpec_Krypt", + rationale = "Propagates IDP auth states to the user." + ) + private fun AuthenticationState.emitAuthState() { + when { + isInProgress() -> { + when (this) { + AuthenticationState.HealthCardCommunicationChannelReady -> + state = State.ReadState.Reading.Reading00 + + AuthenticationState.HealthCardCommunicationTrustedChannelEstablished -> + state = State.ReadState.Reading.Reading25 + + AuthenticationState.HealthCardCommunicationFinished -> + state = State.ReadState.Reading.Reading50 + + AuthenticationState.IDPCommunicationFinished -> + state = State.ReadState.Reading.Reading75 + + else -> {} + } + } + + isFailure() -> { + state = when (this) { + AuthenticationState.HealthCardCommunicationInterrupted -> + State.ReadState.Error.TagLost + + AuthenticationState.HealthCardCardAccessNumberWrong -> + State.ReadState.Error.CardAccessNumberWrong + + AuthenticationState.HealthCardPin2RetriesLeft -> + State.ReadState.Error.PersonalIdentificationWrong(2) + + AuthenticationState.HealthCardPin1RetryLeft -> + State.ReadState.Error.PersonalIdentificationWrong(1) + + AuthenticationState.HealthCardBlocked -> + State.ReadState.Error.HealthCardBlocked + + AuthenticationState.IDPCommunicationFailed -> + State.ReadState.Error.RemoteCommunicationFailed + + AuthenticationState.IDPCommunicationInvalidCertificate -> + State.ReadState.Error.RemoteCommunicationInvalidCertificate + + AuthenticationState.IDPCommunicationInvalidOCSPResponseOfHealthCardCertificate -> + State.ReadState.Error.RemoteCommunicationInvalidOCSP + + else -> + State.ReadState.Error.TagLost + } + } + } + } + + override suspend fun cancelAuthentication() { + requestChannel.trySend(Request.Cancel) + } + + internal suspend fun onCancel() { + requestChannel.send(Request.Cancel) + } + + internal suspend fun onCredentialsEntered(pin: String) { + requestChannel.send(Request.CredentialsEntered(pin)) + } +} + +@Composable +fun rememberHealthCardPromptAuthenticator( + bridge: AuthenticationBridge +): HealthCardPromptAuthenticator { + val activity = LocalContext.current as MainActivity + return remember { + HealthCardPromptAuthenticator(activity, bridge) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt index 67554aa0..1b987d2b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/MiniCardWallController.kt @@ -23,6 +23,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.authentication.model.External +import de.gematik.ti.erp.app.authentication.model.HealthCard +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.None +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.authentication.model.SecureElement import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcHealthCard import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationUseCase @@ -63,24 +69,24 @@ class MiniCardWallController( override suspend fun authenticateFor( profileId: ProfileIdentifier - ): AuthenticationBridge.InitialAuthenticationData { + ): InitialAuthenticationData { val profile = useCase.profileData(profileId).first() return when (val ssoTokenScope = useCase.authenticationData(profileId).first().singleSignOnTokenScope) { - is IdpData.ExternalAuthenticationToken -> AuthenticationBridge.External( + is IdpData.ExternalAuthenticationToken -> External( authenticatorId = ssoTokenScope.authenticatorId, authenticatorName = ssoTokenScope.authenticatorName, profile = profile ) is IdpData.AlternateAuthenticationToken, - is IdpData.AlternateAuthenticationWithoutToken -> AuthenticationBridge.SecureElement(profile = profile) + is IdpData.AlternateAuthenticationWithoutToken -> SecureElement(profile = profile) - is IdpData.DefaultToken -> AuthenticationBridge.HealthCard( + is IdpData.DefaultToken -> HealthCard( can = ssoTokenScope.cardAccessNumber, profile = profile ) - null -> AuthenticationBridge.None(profile = profile) + null -> None(profile = profile) } } @@ -91,7 +97,7 @@ class MiniCardWallController( return authenticationUseCase.authenticateWithSecureElement( profileId = profileId, scope = scope.toIdpScope() - ).flowOn(dispatchers.IO) + ).flowOn(dispatchers.io) } override fun doHealthCardAuthentication( @@ -107,11 +113,11 @@ class MiniCardWallController( can = can, pin = pin, cardChannel = flow { emit(NfcHealthCard.connect(tag)) } - ).flowOn(dispatchers.IO) + ).flowOn(dispatchers.io) } override suspend fun loadExternalAuthenticators(): List = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { idpUseCase.loadExternAuthenticatorIDs() } @@ -120,7 +126,7 @@ class MiniCardWallController( scope: PromptAuthenticator.AuthScope, authenticatorId: String, authenticatorName: String - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { runCatching { idpUseCase.getUniversalLinkForExternalAuthorization( profileId = profileId, @@ -131,14 +137,14 @@ class MiniCardWallController( } } - override suspend fun doExternalAuthorization(redirect: URI): Result = withContext(dispatchers.IO) { + override suspend fun doExternalAuthorization(redirect: URI): Result = withContext(dispatchers.io) { runCatching { idpUseCase.authenticateWithExternalAppAuthorization(redirect) } } override suspend fun doRemoveAuthentication(profileId: ProfileIdentifier) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { idpRepository.invalidate(profileId) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt similarity index 74% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt index 22f254ba..d4316fc7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/mini/ui/SecureHardwarePrompt.kt @@ -25,17 +25,16 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.utils.compose.AcceptDialog -import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import io.github.aakira.napier.Napier import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -47,7 +46,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import io.github.aakira.napier.Napier @Stable class SecureHardwarePromptAuthenticator( @@ -188,51 +186,3 @@ fun rememberSecureHardwarePromptAuthenticator( SecureHardwarePromptAuthenticator(activity, bridge, promptInfo) } } - -@Composable -fun SecureHardwarePrompt( - authenticator: SecureHardwarePromptAuthenticator -) { - val scope = rememberCoroutineScope() - authenticator.showError?.let { error -> - val retryText = when (error) { - is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful -> Pair( - stringResource(R.string.cdw_mini_alt_auth_removed_title).toAnnotatedString(), - stringResource(R.string.cdw_mini_alt_auth_removed).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationFailed -> Pair( - stringResource(R.string.cdw_nfc_intro_step1_header_on_error).toAnnotatedString(), - stringResource(R.string.cdw_idp_error_time_and_connection).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidCertificate -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_certificate).toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_certificate).toAnnotatedString() - ) - - SecureHardwarePromptAuthenticator.Error.RemoteCommunicationInvalidOCSP -> Pair( - stringResource(R.string.cdw_nfc_error_title_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString(), - stringResource(R.string.cdw_nfc_error_body_invalid_ocsp_response_of_health_card_certificate) - .toAnnotatedString() - ) - } - - retryText.let { (title, message) -> - AcceptDialog( - header = title, - info = message, - acceptText = stringResource(R.string.ok), - onClickAccept = { - scope.launch { - if (error is SecureHardwarePromptAuthenticator.Error.RemoteCommunicationAltAuthNotSuccessful) { - authenticator.removeAuthentication(error.profileId) - } - authenticator.resetErrorState() - } - } - ) - } - } -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardChannel.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcCardSecureChannel.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/model/nfc/card/NfcHealthCard.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt index 444e8c07..ac236b43 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/AltPairing.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.secureRandomInstance import io.github.aakira.napier.Napier import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt index e3f7b76c..e2259c78 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAccessNumber.kt @@ -45,9 +45,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt index 857a73fc..2f2a7fa0 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAnimation.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import kotlinx.coroutines.delay diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt index deee2328..06aed5ed 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallAuthDialog.kt @@ -27,7 +27,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -48,6 +48,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -59,25 +60,24 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.ui.platform.LocalContext import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.NfcNotEnabledException -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.trackAuth import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState import de.gematik.ti.erp.app.core.LocalActivity import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.analytics.trackAuth -import de.gematik.ti.erp.app.troubleShooting.TroubleshootingInfo +import de.gematik.ti.erp.app.troubleshooting.TroubleshootingInfo import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.Dialog import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import io.github.aakira.napier.Napier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -86,10 +86,9 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.retryWhen import java.util.Locale import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException @@ -485,7 +484,7 @@ private fun AuthenticationDialog( fun InfoText(showTroubleshooting: Boolean, info: Pair, onClickTroubleshooting: () -> Unit?) { if (showTroubleshooting) { TroubleshootingInfo( - onClick = { onClickTroubleshooting?.run { onClickTroubleshooting() } } + onClick = { onClickTroubleshooting.run { onClickTroubleshooting() } } ) } else { Text( diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt index 639a7108..e300652f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallComponents.kt @@ -31,6 +31,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope 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.LazyListState @@ -49,10 +51,14 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.material.icons.rounded.RadioButtonUnchecked import androidx.compose.runtime.Composable +import androidx.compose.runtime.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.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -69,31 +75,28 @@ import androidx.core.content.ContextCompat.startActivity import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.navOptions -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.domain.biometric.deviceStrongBiometricStatus import de.gematik.ti.erp.app.cardwall.domain.biometric.hasDeviceStrongBox import de.gematik.ti.erp.app.cardwall.domain.biometric.isDeviceSupportsBiometric import de.gematik.ti.erp.app.cardwall.ui.model.CardWallNavigation import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.demomode.DemoModeIntent +import de.gematik.ti.erp.app.demomode.startAppWithDemoMode +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.orderhealthcard.ui.HealthCardContactOrderScreen import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.core.complexAutoSaver -import de.gematik.ti.erp.app.troubleShooting.TroubleShootingScreen +import de.gematik.ti.erp.app.troubleshooting.TroubleShootingScreen import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.HintCard @@ -123,6 +126,7 @@ fun CardWallScreen( val startDestination = CardWallNavigation.Intro.path() val context = LocalContext.current + val activity = LocalActivity.current val biometricMode = remember { deviceStrongBiometricStatus(context) } val navigationMode by navController.navigationModeState( @@ -155,7 +159,7 @@ fun CardWallScreen( } var previousNavEntry by remember { mutableStateOf("cardwall_introduction") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) var cardAccessNumber by rememberSaveable { mutableStateOf("") } @@ -186,6 +190,7 @@ fun CardWallScreen( composable(CardWallNavigation.Intro.route) { NavigationAnimation(mode = navigationMode) { CardWallIntroScaffold( + nfcEnabled = cardWallController.isNFCEnabled(), onNext = { navController.navigate(CardWallNavigation.CardAccessNumber.path()) }, actions = { TextButton(onClick = { onResumeCardWall() }) { @@ -196,7 +201,9 @@ fun CardWallScreen( navController.navigate(CardWallNavigation.ExternalAuthenticator.path()) }, onClickOrderNow = { navController.navigate(CardWallNavigation.OrderHealthCard.path()) }, - nfcEnabled = cardWallController.isNFCEnabled() + onClickDemoMode = { + DemoModeIntent.startAppWithDemoMode(activity = activity) + } ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt index 3d578642..374ed28f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallController.kt @@ -23,6 +23,7 @@ import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.cardwall.model.nfc.card.NfcHealthCard import de.gematik.ti.erp.app.cardwall.usecase.AuthenticationState @@ -31,6 +32,7 @@ import de.gematik.ti.erp.app.cardwall.usecase.CardWallUseCase import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -41,7 +43,8 @@ import org.kodein.di.compose.rememberInstance class CardWallController( private val cardWallUseCase: CardWallUseCase, private val authenticationUseCase: AuthenticationUseCase, - private val dispatchers: DispatchProvider + private val dispatchers: DispatchProvider, + private val scope: CoroutineScope ) { val hardwareRequirementsFulfilled = cardWallUseCase.deviceHasNFCAndAndroidMOrHigher @@ -85,7 +88,7 @@ class CardWallController( cardChannel = cardChannel ) } - .flowOn(dispatchers.IO) + .flowOn(dispatchers.io) } fun isNFCEnabled() = cardWallUseCase.deviceHasNFCEnabled @@ -96,11 +99,13 @@ fun rememberCardWallController(): CardWallController { val cardWallUseCase by rememberInstance() val authenticationUseCase by rememberInstance() val dispatchers by rememberInstance() + val scope = rememberCoroutineScope() return remember { CardWallController( cardWallUseCase = cardWallUseCase, authenticationUseCase = authenticationUseCase, - dispatchers = dispatchers + dispatchers = dispatchers, + scope = scope ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt index 7408a48d..b8332dac 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcInstructionScreen.kt @@ -66,8 +66,8 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.google.accompanist.systemuicontroller.rememberSystemUiController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R 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 diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallNfcPositionState.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt similarity index 91% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt index 654a8a07..75f98765 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallScaffold.kt @@ -49,15 +49,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.ClickText +import de.gematik.ti.erp.app.utils.compose.ClickableText import de.gematik.ti.erp.app.utils.compose.HintTextActionButton import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.SpacerXLarge @@ -69,11 +72,12 @@ import de.gematik.ti.erp.app.utils.compose.SpacerXLarge ) @Composable fun CardWallIntroScaffold( + nfcEnabled: Boolean, + actions: @Composable RowScope.() -> Unit = {}, onNext: () -> Unit, onClickAlternateAuthentication: () -> Unit, onClickOrderNow: () -> Unit, - actions: @Composable RowScope.() -> Unit = {}, - nfcEnabled: Boolean + onClickDemoMode: () -> Unit ) { val activity = LocalActivity.current @@ -95,10 +99,11 @@ fun CardWallIntroScaffold( .padding(it) ) { AddCardContent( + nfcAvailable = nfcEnabled, onClickOrderNow = onClickOrderNow, onClickHealthCardAuth = onNext, onClickInsuranceAuth = onClickAlternateAuthentication, - nfcAvailable = nfcEnabled + onClickDemoMode = onClickDemoMode ) } } @@ -146,10 +151,11 @@ val CardPaddingModifier = Modifier @OptIn(ExperimentalMaterialApi::class) @Composable fun AddCardContent( - onClickOrderNow: () -> Unit, + nfcAvailable: Boolean, onClickHealthCardAuth: () -> Unit, onClickInsuranceAuth: () -> Unit, - nfcAvailable: Boolean + onClickOrderNow: () -> Unit, + onClickDemoMode: () -> Unit ) { Column( modifier = Modifier.padding(PaddingDefaults.Medium), @@ -271,6 +277,18 @@ fun AddCardContent( ) { onClickOrderNow() } + SpacerMedium() + ClickableText( + modifier = Modifier.padding(horizontal = PaddingDefaults.Large), + textWithPlaceholdersRes = R.string.demo_mode_start_text, + textStyle = AppTheme.typography.body1l, + clickText = ClickText( + text = stringResource(R.string.demo_mode_link_text), + onClick = { + onClickDemoMode() + } + ) + ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt index ad10b57e..cc52f1bb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/CardWallSecret.kt @@ -53,9 +53,9 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt index 1b108b50..45c19df3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListScreen.kt @@ -63,22 +63,23 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.api.models.AuthenticationId +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.rememberProfileHandler import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import de.gematik.ti.erp.app.utils.compose.SpacerLarge import org.kodein.di.compose.rememberViewModel @Composable @@ -89,6 +90,7 @@ fun ExternalAuthenticatorListScreen( onBack: () -> Unit ) { val viewModel by rememberViewModel() + val controller = rememberProfilesController() val listState = rememberLazyListState() AnimatedElevationScaffold( navigationMode = NavigationBarMode.Back, @@ -103,6 +105,7 @@ fun ExternalAuthenticatorListScreen( ) { AuthenticatorList( profileId = profileId, + controller = controller, viewModel = viewModel, onNext = onNext, listState = listState @@ -148,6 +151,7 @@ private sealed interface RefreshState { fun AuthenticatorList( profileId: ProfileIdentifier, viewModel: ExternalAuthenticatorListViewModel, + controller: ProfilesController, onNext: () -> Unit, listState: LazyListState ) { @@ -167,7 +171,6 @@ fun AuthenticatorList( } val coroutineScope = rememberCoroutineScope() - val profileHandler = rememberProfileHandler() var search by remember { mutableStateOf("") } val externalAuthenticatorListFiltered by rememberFilteredAuthenticatorsList( @@ -236,7 +239,7 @@ fun AuthenticatorList( ) intentHandler.startFastTrackApp(redirectUri) if (it.id.endsWith("pkv")) { - profileHandler.switchProfileToPKV(profileId) + controller.switchToPrivateInsurance(profileId) } onNext() } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt index 20549201..82cdd6a4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/ExternalAuthenticatorListViewModel.kt @@ -31,7 +31,7 @@ class ExternalAuthenticatorListViewModel( private val dispatchers: DispatchProvider ) : ViewModel() { - suspend fun externalAuthenticatorIDList() = withContext(dispatchers.IO) { + suspend fun externalAuthenticatorIDList() = withContext(dispatchers.io) { idpUseCase.loadExternAuthenticatorIDs() } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/ui/model/Navigation.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/AuthenticationUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt index 4a771ad2..b8fef4e8 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallLoadNfcPositionUseCase.kt @@ -20,9 +20,8 @@ package de.gematik.ti.erp.app.cardwall.usecase import android.content.Context import android.os.Build -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.cardwall.usecase.model.NfcPositionUseCaseData -import kotlinx.serialization.decodeFromString +import de.gematik.ti.erp.app.features.R import kotlinx.serialization.json.Json import java.io.InputStream diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt index 053ead58..15c7eec1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/CardWallUseCase.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.cardwall.usecase import android.content.Context import android.nfc.NfcAdapter -import de.gematik.ti.erp.app.app +import de.gematik.ti.erp.app.ErezeptApp.Companion.applicationModule import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier @@ -32,13 +32,13 @@ open class CardWallUseCase( private val cardWallRepository: CardWallRepository ) { var deviceHasNFCAndAndroidMOrHigher: Boolean - get() = app().deviceHasNFC() || cardWallRepository.hasFakeNFCEnabled + get() = applicationModule.androidContext().deviceHasNFC() || cardWallRepository.hasFakeNFCEnabled set(value) { cardWallRepository.hasFakeNFCEnabled = value } val deviceHasNFCEnabled - get() = app().nfcEnabled() + get() = applicationModule.androidContext().nfcEnabled() fun authenticationData(profileId: ProfileIdentifier): Flow = idpRepository.authenticationData(profileId) diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt similarity index 68% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt index 9c97163a..880a0836 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/MiniCardWallUseCase.kt @@ -21,20 +21,26 @@ package de.gematik.ti.erp.app.cardwall.usecase import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext class MiniCardWallUseCase( private val idpRepository: IdpRepository, - private val profilesUseCase: ProfilesUseCase + private val getProfilesUseCase: GetProfilesUseCase, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { fun authenticationData(profileId: ProfileIdentifier): Flow = idpRepository.authenticationData(profileId) - fun profileData(profileId: ProfileIdentifier): Flow = - profilesUseCase.profiles.map { profiles -> - requireNotNull(profiles.find { it.id == profileId }) { "Profile `$profileId` missing!" } + suspend fun profileData(profileId: ProfileIdentifier): Flow = + withContext(dispatcher) { + getProfilesUseCase().map { profiles -> + requireNotNull(profiles.find { it.id == profileId }) { "Profile `$profileId` missing!" } + } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/cardwall/usecase/model/NfcPositionUseCaseData.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt similarity index 84% rename from android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt index a0fdac9e..1f665687 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/core/MainComposable.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppContent.kt @@ -16,6 +16,8 @@ * */ +@file:Suppress("MagicNumber") + package de.gematik.ti.erp.app.core import androidx.activity.ComponentActivity @@ -28,6 +30,7 @@ import androidx.compose.foundation.gestures.calculatePan import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -49,13 +52,20 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.toSize import 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.MainActivity import de.gematik.ti.erp.app.analytics.Analytics +import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator +import de.gematik.ti.erp.app.demomode.DemoModeIntent +import de.gematik.ti.erp.app.demomode.startAppWithNormalMode +import de.gematik.ti.erp.app.demomode.ui.DemoModeStatusBar +import de.gematik.ti.erp.app.demomode.ui.checkForDemoMode +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.settings.ui.rememberSettingsController +import de.gematik.ti.erp.app.theme.AppTheme import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope @@ -74,7 +84,7 @@ val LocalAnalytics = staticCompositionLocalOf { error("No analytics provided!") } @Composable -fun MainContent( +fun AppContent( content: @Composable (settingsController: SettingsController) -> Unit ) { val settingsController = rememberSettingsController() @@ -83,15 +93,28 @@ fun MainContent( AppTheme { val systemUiController = rememberSystemUiController() val useDarkIcons = MaterialTheme.colors.isLight + val activity = LocalActivity.current SideEffect { systemUiController.setSystemBarsColor(Color.Transparent, darkIcons = useDarkIcons) } - - Box( - modifier = Modifier.zoomable(enabled = zoomState.zoomEnabled) - ) { - content(settingsController) - } + checkForDemoMode( + demoModeStatusBarColor = AppTheme.colors.yellow500, + demoModeContent = { + DemoModeStatusBar( + modifier = Modifier.fillMaxWidth(), + backgroundColor = AppTheme.colors.yellow500, + textColor = AppTheme.colors.neutral900, + demoModeActiveText = stringResource(R.string.demo_mode_text), + demoModeEndText = stringResource(R.string.demo_mode_cancel_button_text), + onClickDemoModeEnd = { DemoModeIntent.startAppWithNormalMode(activity) } + ) + }, + appContent = { + Box(modifier = Modifier.zoomable(enabled = zoomState.zoomEnabled)) { + content(settingsController) + } + } + ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt index 853eab17..1139c645 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/core/AppScopedCache.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/AppScopedCache.kt @@ -19,7 +19,8 @@ package de.gematik.ti.erp.app.core import androidx.compose.runtime.saveable.Saver -import de.gematik.ti.erp.app.App +import de.gematik.ti.erp.app.ErezeptApp +import io.github.aakira.napier.Napier import java.util.UUID class AppScopedCache { @@ -45,12 +46,13 @@ fun complexAutoSaver( ): Saver = Saver( save = { state -> val key = UUID.randomUUID().toString() - App.cache.store(key, state) + ErezeptApp.cache.store(key, state) key }, restore = { key -> @Suppress("UNCHECKED_CAST") - (App.cache.recover(key) as T).apply { + (ErezeptApp.cache.recover(key) as T).apply { + Napier.d { "AppScopeData cache value $this" } init() } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/core/IntentHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/core/IntentHandler.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/core/IntentHandler.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt new file mode 100644 index 00000000..a89109a9 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/ApplicationModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +import android.app.Application +import android.content.Context +import android.content.res.Resources + +class ApplicationModule(private val application: Application) { + fun androidApplication(): Application = application + fun androidContext(): Context = application.applicationContext + fun androidResources(): Resources = application.resources +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt new file mode 100644 index 00000000..5e5c9140 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/FeatureModule.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.di + +import de.gematik.ti.erp.app.attestation.usecase.integrityModule +import de.gematik.ti.erp.app.authentication.di.authenticationModule +import de.gematik.ti.erp.app.cardunlock.cardUnlockModule +import de.gematik.ti.erp.app.cardwall.cardWallModule +import de.gematik.ti.erp.app.idp.idpModule +import de.gematik.ti.erp.app.idp.idpUseCaseModule +import de.gematik.ti.erp.app.orderhealthcard.orderHealthCardModule +import de.gematik.ti.erp.app.orders.messageRepositoryModule +import de.gematik.ti.erp.app.orders.messagesModule +import de.gematik.ti.erp.app.pharmacy.di.pharmacyModule +import de.gematik.ti.erp.app.pharmacy.di.pharmacyRepositoryModule +import de.gematik.ti.erp.app.pkv.pkvModule +import de.gematik.ti.erp.app.prescription.prescriptionModule +import de.gematik.ti.erp.app.prescription.prescriptionRepositoryModule +import de.gematik.ti.erp.app.prescription.taskModule +import de.gematik.ti.erp.app.prescription.taskRepositoryModule +import de.gematik.ti.erp.app.profiles.profileRepositoryModule +import de.gematik.ti.erp.app.profiles.profilesModule +import de.gematik.ti.erp.app.protocol.protocolModule +import de.gematik.ti.erp.app.protocol.protocolRepositoryModule +import de.gematik.ti.erp.app.redeem.redeemModule +import de.gematik.ti.erp.app.settings.settingsModule +import de.gematik.ti.erp.app.vau.vauModule +import org.kodein.di.DI + +val featureModule = DI.Module("featureModule", allowSilentOverride = true) { + importAll( + cardWallModule, + integrityModule, + networkModule, + realmModule, + idpModule, + idpUseCaseModule, + messagesModule, + orderHealthCardModule, + pharmacyModule, + redeemModule, + prescriptionModule, + profilesModule, + protocolModule, + taskModule, + settingsModule, + vauModule, + cardUnlockModule, + pkvModule, + authenticationModule, + profileRepositoryModule, + prescriptionRepositoryModule, + protocolRepositoryModule, + pharmacyRepositoryModule, + messageRepositoryModule, + taskRepositoryModule, + allowOverride = true + ) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/di/NetworkModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/di/NetworkModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/di/RealmModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/di/RealmModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt index 1e8f303e..ed5b3016 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/featuretoggle/FeatureToggleManager.kt @@ -31,6 +31,7 @@ enum class Features(val featureName: String) { // REDEEM_WITHOUT_TI("RedeemWithoutTI"), } +// TODO: Poor naming, change it class FeatureToggleManager(val context: Context) { private val dataStore = context.dataStore diff --git a/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt similarity index 90% rename from android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt index 26b11461..e8c04bed 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/idp/IdpModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/idp/IdpModule.kt @@ -19,11 +19,11 @@ 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 import de.gematik.ti.erp.app.idp.repository.IdpRemoteDataSource import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.idp.usecase.DefaultIdpUseCase import de.gematik.ti.erp.app.idp.usecase.IdpAlternateAuthenticationUseCase import de.gematik.ti.erp.app.idp.usecase.IdpBasicUseCase import de.gematik.ti.erp.app.idp.usecase.IdpCryptoProvider @@ -35,6 +35,8 @@ import org.kodein.di.bindProvider import org.kodein.di.bindSingleton import org.kodein.di.instance +const val NetworkSecurePreferencesTag = "NetworkSecurePreferences" + val idpModule = DI.Module("idpModule") { bindProvider { IdpLocalDataSource(instance()) } bindProvider { IdpPairingRepository(instance()) } @@ -42,7 +44,6 @@ val idpModule = DI.Module("idpModule") { val endpointHelper = instance() IdpRemoteDataSource(instance()) { endpointHelper.getIdpScope() } } - bindProvider { IdpAlternateAuthenticationUseCase(instance(), instance(), instance()) } bindProvider { IdpCryptoProvider() } bindProvider { IdpDeviceInfoProvider() } bindProvider { @@ -51,9 +52,12 @@ val idpModule = DI.Module("idpModule") { } } bindSingleton { IdpRepository(instance(), instance()) } - bindSingleton { IdpBasicUseCase(instance(), instance()) } - bindSingleton { - IdpUseCase( +} + +val idpUseCaseModule = DI.Module("idpUseCaseModule", allowSilentOverride = true) { + bindProvider { IdpAlternateAuthenticationUseCase(instance(), instance(), instance()) } + bindProvider { + DefaultIdpUseCase( repository = instance(), pairingRepository = instance(), altAuthUseCase = instance(), @@ -63,4 +67,5 @@ val idpModule = DI.Module("idpModule") { cryptoProvider = instance() ) } + bindSingleton { IdpBasicUseCase(instance(), instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt new file mode 100644 index 00000000..230945a3 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/info/BuildConfigInformation.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.info + +import android.content.Context +import androidx.compose.runtime.Composable + +interface BuildConfigInformation { + fun versionName(): String + fun versionCode(): String + fun model(): String + fun language(): String + + @Composable + fun inDarkTheme(): String + fun nfcInformation(context: Context): String +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt index 18731a7a..7b5c6455 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/interceptor/HeadersInterceptor.kt @@ -23,12 +23,12 @@ import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.di.EndpointHelper import de.gematik.ti.erp.app.idp.usecase.IdpUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import java.net.HttpURLConnection +import io.github.aakira.napier.Napier import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import io.github.aakira.napier.Napier +import java.net.HttpURLConnection private const val invalidAccessTokenHeader = "Www-Authenticate" private const val invalidAccessTokenValue = "Bearer realm='prescriptionserver.telematik', error='invalACCESS_TOKEN'" @@ -59,7 +59,7 @@ class BearerHeaderInterceptor( private fun loadAccessToken(refresh: Boolean, profileId: ProfileIdentifier?) = runBlocking { - idpUseCase.loadAccessToken(refresh, profileId ?: error("no profile id given")) + idpUseCase.loadAccessToken(refresh = refresh, profileId = profileId ?: error("no profile id given")) } private fun request(original: Request, token: String) = diff --git a/android/src/main/java/de/gematik/ti/erp/app/license/model/LicenseModels.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/license/model/LicenseModels.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/license/model/LicenseModels.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/license/model/LicenseModels.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt index 45cf68c7..4f373215 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/license/ui/LicenseScreen.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.license.model.License import de.gematik.ti.erp.app.license.model.LicenseEntry import de.gematik.ti.erp.app.license.model.parseLicenses diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt new file mode 100644 index 00000000..56431e70 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenContentNavHost.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.navigation + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.orders.ui.OrderScreen +import de.gematik.ti.erp.app.prescription.ui.PrescriptionsScreen +import de.gematik.ti.erp.app.prescription.ui.rememberPrescriptionsController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController +import de.gematik.ti.erp.app.settings.ui.SettingsController +import de.gematik.ti.erp.app.settings.ui.SettingsScreen + +@Composable +internal fun MainScreenContentNavHost( + modifier: Modifier, + mainScreenController: MainScreenController, + settingsController: SettingsController, + mainNavController: NavController, + bottomNavController: NavHostController, + onElevateTopBar: (Boolean) -> Unit, + onClickAvatar: () -> Unit, + onClickArchive: () -> Unit +) { + Box( + modifier = modifier + .testTag("main_screen") + ) { + NavHost( + bottomNavController, + startDestination = MainNavigationScreens.Prescriptions.path() + ) { + composable(MainNavigationScreens.Prescriptions.route) { + val prescriptionsController = rememberPrescriptionsController() + val profilesController = rememberProfilesController() + val activeProfile by profilesController.getActiveProfileState() + PrescriptionsScreen( + controller = prescriptionsController, + mainScreenController = mainScreenController, + activeProfile = activeProfile, + onElevateTopBar = onElevateTopBar, + onClickArchive = onClickArchive, + onClickAvatar = onClickAvatar, + onShowCardWall = { + mainNavController.navigate( + MainNavigationScreens.CardWall.path(activeProfile.id) + ) + }, + onClickPrescription = { taskId -> + mainNavController.navigate( + MainNavigationScreens.PrescriptionDetail.path( + taskId = taskId + ) + ) + } + ) + } + composable(MainNavigationScreens.Orders.route) { + OrderScreen( + mainNavController = mainNavController, + mainScreenController = mainScreenController, + onElevateTopBar = onElevateTopBar + ) + } + composable( + MainNavigationScreens.Settings.route, + MainNavigationScreens.Settings.arguments + ) { + SettingsScreen( + mainNavController = mainNavController, + settingsController = settingsController + ) + } + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt similarity index 52% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt index 8593adb1..9cdf8f9a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigation.kt @@ -16,112 +16,54 @@ * */ -package de.gematik.ti.erp.app.mainscreen.ui +package de.gematik.ti.erp.app.mainscreen.navigation -import android.net.Uri -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.AppBarDefaults -import androidx.compose.material.Badge -import androidx.compose.material.BadgedBox -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ModalBottomSheetLayout -import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Scaffold -import androidx.compose.material.ScaffoldState -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.PinDrop -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.ShoppingBag -import androidx.compose.material.icons.rounded.AddCircle -import androidx.compose.material.rememberModalBottomSheetState -import androidx.compose.material.rememberScaffoldState +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -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.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color.Companion.Red -import androidx.compose.ui.graphics.Color.Companion.White -import androidx.compose.ui.layout.boundsInRoot -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import de.gematik.ti.erp.app.BuildConfig import de.gematik.ti.erp.app.LegalNoticeWithScaffold -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.analytics.TrackPopUps -import de.gematik.ti.erp.app.analytics.trackMainScreenBottomPopUps -import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackPopUps import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.cardunlock.ui.UnlockEgKScreen import de.gematik.ti.erp.app.cardwall.ui.CardWallScreen import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.debug.ui.DebugScreenWrapper +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.license.ui.LicenseScreen +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.mainscreen.ui.InsecureDeviceScreen +import de.gematik.ti.erp.app.mainscreen.ui.MainScreenScaffoldContainer import de.gematik.ti.erp.app.onboarding.ui.OnboardingNavigationScreens import de.gematik.ti.erp.app.onboarding.ui.OnboardingScreen 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 import de.gematik.ti.erp.app.pharmacy.ui.PharmacyNavigation import de.gematik.ti.erp.app.prescription.detail.ui.PrescriptionDetailsScreen 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.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.ui.RefreshedState import de.gematik.ti.erp.app.prescription.ui.ScanScreen import de.gematik.ti.erp.app.prescription.ui.rememberPrescriptionsController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.profiles.ui.EditProfileScreen -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.profiles.ui.ProfileImageCropper -import de.gematik.ti.erp.app.profiles.ui.ProfilesStateData -import de.gematik.ti.erp.app.profiles.ui.rememberProfilesController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById import de.gematik.ti.erp.app.redeem.ui.RedeemNavigation import de.gematik.ti.erp.app.settings.ui.AllowAnalyticsScreen import de.gematik.ti.erp.app.settings.ui.PharmacyLicenseScreen @@ -129,26 +71,13 @@ import de.gematik.ti.erp.app.settings.ui.SecureAppWithPassword import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.settings.ui.SettingsScreen import de.gematik.ti.erp.app.settings.ui.rememberSettingsController -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.BottomNavigation import de.gematik.ti.erp.app.utils.compose.NavigationAnimation -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.compose.TopAppBarWithContent import de.gematik.ti.erp.app.utils.compose.navigationModeState import de.gematik.ti.erp.app.webview.URI_DATA_TERMS import de.gematik.ti.erp.app.webview.URI_TERMS_OF_USE import de.gematik.ti.erp.app.webview.WebViewScreen -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private const val BottomBarBadgeOffsetX = -5 -private const val BottomBarBadgeOffsetY = 5 +@RequiresApi(Build.VERSION_CODES.O) @Requirement( "A_19178", sourceSpecification = "gemSpec_eRp_FdV", @@ -519,7 +448,7 @@ private const val BottomBarBadgeOffsetY = 5 ) @Suppress("LongMethod") @Composable -fun MainScreen( +fun MainScreenNavigation( navController: NavHostController ) { val settingsController = rememberSettingsController() @@ -533,9 +462,9 @@ fun MainScreen( } val analytics = LocalAnalytics.current val analyticsState by analytics.screenState - TrackPopUps(analytics, analyticsState) + trackPopUps(analytics, analyticsState) var previousNavEntry by remember { mutableStateOf("main") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by navController.navigationModeState(OnboardingNavigationScreens.Onboarding.route) NavHost( navController, @@ -580,14 +509,13 @@ fun MainScreen( sourceSpecification = "BSI-eRp-ePA", rationale = "Check for insecure Devices on MainScreen." ) - MainScreenWithScaffold( + MainScreenScaffoldContainer( mainNavController = navController, onDeviceIsInsecure = { navController.navigate(MainNavigationScreens.IntegrityNotOkScreen.path()) } ) } - composable( MainNavigationScreens.PrescriptionDetail.route, MainNavigationScreens.PrescriptionDetail.arguments @@ -785,19 +713,15 @@ fun MainScreen( MainNavigationScreens.EditProfile.arguments ) { val profilesController = rememberProfilesController() - val profileId = remember { it.arguments!!.getString("profileId")!! } - val scope = rememberCoroutineScope() - val profilesState by profilesController.profilesState + val profileId = remember { it.arguments?.getString("profileId") } + val profiles by profilesController.getProfilesState() - profilesState.profileById(profileId)?.let { profile -> + profiles.profileById(profileId)?.let { profile -> EditProfileScreen( - profilesState, profile, profilesController, onRemoveProfile = { - scope.launch { - profilesController.removeProfile(profile, it) - } + profilesController.removeProfile(profile, it) navController.popBackStack() }, onBack = { navController.popBackStack() }, @@ -812,13 +736,10 @@ fun MainScreen( ) { val profileId = remember { it.arguments!!.getString("profileId")!! } val profilesController = rememberProfilesController() - val scope = rememberCoroutineScope() ProfileImageCropper( onSaveCroppedImage = { - scope.launch { - profilesController.savePersonalizedProfileImage(profileId, it) - navController.navigate(MainNavigationScreens.Prescriptions.path()) - } + profilesController.savePersonalizedProfileImage(profileId, it) + navController.navigate(MainNavigationScreens.Prescriptions.path()) }, onBack = { navController.popBackStack() @@ -875,459 +796,6 @@ private fun checkFirstAppStart(settingsController: SettingsController) = MainNavigationScreens.Prescriptions.route } -@OptIn(ExperimentalMaterialApi::class) -@Suppress("LongMethod") -@Composable -private fun MainScreenWithScaffold( - mainNavController: NavController, - onDeviceIsInsecure: () -> Unit -) { - val mainScreenController = rememberMainScreenController() - val settingsController = rememberSettingsController() - val context = LocalContext.current - val bottomNavController = rememberNavController() - val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsState(null) - var previousNavEntry by remember { mutableStateOf("main") } - TrackNavigationChanges(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) - val isInPrescriptionScreen by remember { - derivedStateOf { - currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route - } - } - @Requirement( - "O.Plat_1#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check for insecure Devices on MainScreen." - ) - CheckInsecureDevice(onDeviceIsInsecure) - @Requirement( - "O.Arch_6#2", - "O.Resi_2#2", - "O.Resi_3#2", - "O.Resi_4#2", - "O.Resi_5#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Check device integrity." - ) - CheckDeviceIntegrity(mainScreenController, mainNavController) - val scaffoldState = rememberScaffoldState() - val scope = rememberCoroutineScope() - MainScreenSnackbar( - mainScreenController = mainScreenController, - scaffoldState = scaffoldState - ) - OrderSuccessHandler(mainScreenController) - var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } - val sheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true - ) - if (sheetState.currentValue != ModalBottomSheetValue.Hidden) { - DisposableEffect(Unit) { - onDispose { - scope.launch { - settingsController.welcomeDrawerShown() - } - } - } - } - LaunchedEffect(mainScreenBottomSheetContentState) { - if (mainScreenBottomSheetContentState != null) { - sheetState.show() - } else { - sheetState.hide() - } - } - val analytics = LocalAnalytics.current - val analyticsState by analytics.screenState - LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - mainScreenBottomSheetContentState?.let { analytics.trackMainScreenBottomPopUps(it) } - } else { - analytics.onPopUpClosed() - val route = Uri.parse(mainNavController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) - } - } - LaunchedEffect(Unit) { - if (settingsController.showWelcomeDrawer.first()) { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() - } - } - LaunchedEffect(sheetState.isVisible) { - if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { - if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Welcome()) { - settingsController.welcomeDrawerShown() - } - mainScreenBottomSheetContentState = null - } - } - LaunchedEffect(Unit) { - if (settingsController.talkbackEnabled(context)) { - settingsController.mainScreenTooltipsShown() - } - } - var profileToRename by remember { - mutableStateOf(ProfilesStateData.defaultProfile) - } - val toolTipBounds = remember { - mutableStateOf>(emptyMap()) - } - ToolTips(settingsController, isInPrescriptionScreen, toolTipBounds) - val coroutineScope = rememberCoroutineScope() - BackHandler(enabled = sheetState.isVisible) { - coroutineScope.launch { - sheetState.hide() - } - } - ModalBottomSheetLayout( - sheetState = sheetState, - modifier = Modifier - .imePadding() - .testTag(TestTag.Main.MainScreenBottomSheet.Modal), - sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) }, - sheetContent = { - MainScreenBottomSheetContentState( - infoContentState = mainScreenBottomSheetContentState, - mainNavController = mainNavController, - profileToRename = profileToRename, - onCancel = { - coroutineScope.launch { - sheetState.hide() - } - } - ) - } - ) { - // TODO: move to general place? - ExternalAuthenticationDialog() - MainScreenScaffold( - mainScreenController = mainScreenController, - settingsController = settingsController, - mainNavController = mainNavController, - bottomNavController = bottomNavController, - tooltipBounds = toolTipBounds, - onClickAddProfile = { - mainScreenBottomSheetContentState = - MainScreenBottomSheetContentState.AddProfile() - }, - onClickChangeProfileName = { profile -> - profileToRename = profile - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfileName() - }, - onClickAvatar = { - mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfilePicture() - }, - scaffoldState = scaffoldState - ) - } -} - -@Composable -private fun MainScreenScaffold( - mainScreenController: MainScreenController, - settingsController: SettingsController, - mainNavController: NavController, - bottomNavController: NavHostController, - tooltipBounds: MutableState>, - onClickAddProfile: () -> Unit, - onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, - onClickAvatar: () -> Unit, - scaffoldState: ScaffoldState -) { - val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsState(null) - - val isInPrescriptionScreen by remember { - derivedStateOf { - currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route - } - } - var topBarElevated by remember { mutableStateOf(true) } - - Scaffold( - modifier = Modifier.testTag(TestTag.Main.MainScreen), - topBar = { - if (currentBottomNavigationRoute?.destination?.route != MainNavigationScreens.Settings.route) { - MultiProfileTopAppBar( - navController = mainNavController, - elevated = topBarElevated, - settingsController = settingsController, - mainScreenController = mainScreenController, - isInPrescriptionScreen = isInPrescriptionScreen, - onClickAddProfile = onClickAddProfile, - onClickChangeProfileName = onClickChangeProfileName, - tooltipBounds = tooltipBounds - ) - } - }, - bottomBar = { - MainScreenBottomBar( - navController = mainNavController, - mainScreenController = mainScreenController, - bottomNavController = bottomNavController - ) - }, - floatingActionButton = { - if (isInPrescriptionScreen) { - RedeemFloatingActionButton( - onClick = { - mainNavController.navigate(MainNavigationScreens.Redeem.path()) - } - ) - } - }, - scaffoldState = scaffoldState - ) { innerPadding -> - - MainScreenBottomNavHost( - mainScreenController = mainScreenController, - settingsController = settingsController, - mainNavController = mainNavController, - bottomNavController = bottomNavController, - innerPadding = innerPadding, - onClickAvatar = onClickAvatar, - onElevateTopBar = { - topBarElevated = it - }, - onClickArchive = { mainNavController.navigate(MainNavigationScreens.Archive.path()) } - ) - } -} - -@Composable -private fun MainScreenBottomNavHost( - mainScreenController: MainScreenController, - settingsController: SettingsController, - mainNavController: NavController, - bottomNavController: NavHostController, - innerPadding: PaddingValues, - onClickAvatar: () -> Unit, - onElevateTopBar: (Boolean) -> Unit, - onClickArchive: () -> Unit -) { - Box( - modifier = Modifier - .padding(innerPadding) - .testTag("main_screen") - ) { - NavHost( - bottomNavController, - startDestination = MainNavigationScreens.Prescriptions.path() - ) { - composable(MainNavigationScreens.Prescriptions.route) { - val prescriptionsController = rememberPrescriptionsController() - PrescriptionScreen( - navController = mainNavController, - onClickAvatar = onClickAvatar, - prescriptionsController = prescriptionsController, - mainScreenController = mainScreenController, - onElevateTopBar = onElevateTopBar, - onClickArchive = onClickArchive - ) - } - composable(MainNavigationScreens.Orders.route) { - OrderScreen( - mainNavController = mainNavController, - mainScreenController = mainScreenController, - onElevateTopBar = onElevateTopBar - ) - } - composable( - MainNavigationScreens.Settings.route, - MainNavigationScreens.Settings.arguments - ) { - SettingsScreen( - mainNavController = mainNavController, - settingsController = settingsController - ) - } - } - } -} - -@Composable -private fun CheckDeviceIntegrity( - mainScreenController: MainScreenController, - mainNavController: NavController -) { - LaunchedEffect(Unit) { - if (BuildConfig.DEBUG) { - return@LaunchedEffect - } - if (mainScreenController.checkDeviceIntegrity().first()) { - withContext(Dispatchers.Main) { - mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) - navOptions { - launchSingleTop = true - popUpTo(MainNavigationScreens.IntegrityNotOkScreen.path()) { - inclusive = true - } - } - } - } - } -} - -@Composable -private fun CheckInsecureDevice(onDeviceIsInsecure: () -> Unit) { - val settingsController = rememberSettingsController() - LaunchedEffect(Unit) { - if (BuildConfig.DEBUG) { - return@LaunchedEffect - } - @Requirement( - "O.Plat_1#3", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Navigate to insecure Devices warning." - ) - withContext(Dispatchers.Main) { - if (settingsController.showInsecureDevicePrompt.first()) { - onDeviceIsInsecure() - } - } - } -} - -@Suppress("LongMethod") -@Composable -private fun MainScreenBottomBar( - navController: NavController, - bottomNavController: NavController, - mainScreenController: MainScreenController -) { - val navBackStackEntry by bottomNavController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - val profileHandler = LocalProfileHandler.current - val profileId = profileHandler.activeProfile.id - var unreadPrescriptionCount = calculatePrescriptionCount(mainScreenController) - - val unreadOrdersCount by mainScreenController.unreadOrders(profileId) - .collectAsState(initial = 0) - - BottomNavigation( - backgroundColor = MaterialTheme.colors.surface, - extraContent = {} - ) { - MainScreenBottomNavigationItems.forEach { screen -> - BottomNavigationItem( - modifier = Modifier.testTag( - when (screen) { - MainNavigationScreens.Prescriptions -> TestTag.BottomNavigation.PrescriptionButton - MainNavigationScreens.Orders -> TestTag.BottomNavigation.OrdersButton - MainNavigationScreens.Pharmacies -> TestTag.BottomNavigation.PharmaciesButton - MainNavigationScreens.Settings -> TestTag.BottomNavigation.SettingsButton - else -> "" - } - ), - selectedContentColor = AppTheme.colors.primary700, - unselectedContentColor = AppTheme.colors.neutral600, - icon = { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - when (screen) { - MainNavigationScreens.Prescriptions -> - if (unreadPrescriptionCount > 0) { - BadgedBox( - badge = { - Badge( - modifier = Modifier.offset( - x = BottomBarBadgeOffsetX.dp, - y = BottomBarBadgeOffsetY.dp - ), - backgroundColor = Red, - contentColor = White - ) { Text(unreadPrescriptionCount.toString()) } - } - ) { - Icon( - painterResource(R.drawable.ic_logo_outlined), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - } else { - Icon( - painterResource(R.drawable.ic_logo_outlined), - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - - MainNavigationScreens.Pharmacies -> Icon( - Icons.Outlined.PinDrop, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - - MainNavigationScreens.Orders -> - if (unreadOrdersCount > 0) { - BadgedBox( - badge = { - Badge( - modifier = Modifier.offset( - x = BottomBarBadgeOffsetX.dp, - y = BottomBarBadgeOffsetY.dp - ), - backgroundColor = Red, - contentColor = White - ) { Text(unreadOrdersCount.toString()) } - } - ) { - Icon( - Icons.Outlined.ShoppingBag, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - } else { - Icon( - Icons.Outlined.ShoppingBag, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - - MainNavigationScreens.Settings -> Icon( - Icons.Outlined.Settings, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - } - }, - label = { - Text( - stringResource( - when (screen) { - MainNavigationScreens.Prescriptions -> R.string.pres_bottombar_prescriptions - MainNavigationScreens.Orders -> R.string.pres_bottombar_orders - MainNavigationScreens.Pharmacies -> R.string.pres_bottombar_pharmacies - MainNavigationScreens.Settings -> R.string.main_settings_acc - else -> R.string.pres_bottombar_prescriptions - } - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - selected = currentRoute == screen.route, - alwaysShowLabel = true, - onClick = { - if (currentRoute != screen.route) { - when (screen.route) { - MainNavigationScreens.Pharmacies.route -> - navController.navigate(screen.path()) - - else -> - bottomNavController.navigate(screen.path()) - } - } - } - ) - } - } -} - @Composable fun calculatePrescriptionCount(mainScreenController: MainScreenController): Int { var prescriptionCount = 0 @@ -1347,152 +815,3 @@ fun calculatePrescriptionCount(mainScreenController: MainScreenController): Int } return prescriptionCount } - -@Composable -private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { - val text = if (isInPrescriptionScreen) { - stringResource(R.string.pres_bottombar_prescriptions) - } else { - stringResource(R.string.orders_title) - } - Text( - text = text, - style = AppTheme.typography.h5, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) -} - -@Composable -private fun ProfilesChipBar( - mainScreenController: MainScreenController, - onClickAddProfile: () -> Unit, - onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, - tooltipBounds: MutableState>, - toolTipBoundsRequired: Boolean -) { - val profileHandler = LocalProfileHandler.current - val profiles = profileHandler.profiles.value - val scope = rememberCoroutineScope() - val rowState = rememberLazyListState() - - var indexOfActiveProfile by remember { mutableStateOf(0) } - - LaunchedEffect(indexOfActiveProfile) { - delay(timeMillis = 300L) - rowState.animateScrollToItem(indexOfActiveProfile) - } - - LazyRow( - state = rowState, - horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), - modifier = Modifier - .fillMaxWidth() - .padding(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Small), - verticalAlignment = Alignment.CenterVertically - ) { - item { - SpacerSmall() - } - profiles.forEachIndexed { index, profile -> - if (profile.id == profileHandler.activeProfile.id) { - indexOfActiveProfile = index + 1 - } - - item { - ProfileChip( - profile = profile, - mainScreenController = mainScreenController, - selected = profile.id == profileHandler.activeProfile.id, - onClickChip = { scope.launch { profileHandler.switchActiveProfile(profile) } }, - onClickChangeProfileName = onClickChangeProfileName, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired - ) - SpacerSmall() - } - } - item { - AddProfileChip( - onClickAddProfile = onClickAddProfile, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired - ) - SpacerMedium() - } - } -} - -/** - * The top appbar of the actual main screen. - */ -@Composable -private fun MultiProfileTopAppBar( - navController: NavController, - mainScreenController: MainScreenController, - settingsController: SettingsController, - isInPrescriptionScreen: Boolean, - elevated: Boolean, - onClickAddProfile: () -> Unit, - onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, - tooltipBounds: MutableState> -) { - val accScan = stringResource(R.string.main_scan_acc) - val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else 0.dp } - - val toolTipBoundsRequired by produceState(initialValue = false) { - settingsController.showMainScreenToolTips().collect { - value = it - } - } - - val scope = rememberCoroutineScope() - - TopAppBarWithContent( - title = { - MainScreenTopBarTitle(isInPrescriptionScreen) - }, - elevation = elevation, - backgroundColor = AppTheme.colors.neutral025, - actions = @Composable { - if (isInPrescriptionScreen) { - // data matrix code scanner - IconButton( - onClick = { - scope.launch { - if (settingsController.mlKitNotAccepted().first()) { - navController.navigate(MainNavigationScreens.MlKitIntroScreen.path()) - } else { - navController.navigate(MainNavigationScreens.Camera.path()) - } - } - }, - modifier = Modifier - .testTag("erx_btn_scn_prescription") - .semantics { contentDescription = accScan } - .onGloballyPositioned { coordinates -> - if (toolTipBoundsRequired) { - tooltipBounds.value += Pair(0, coordinates.boundsInRoot()) - } - } - ) { - Icon( - imageVector = Icons.Rounded.AddCircle, - contentDescription = null, - tint = AppTheme.colors.primary700, - modifier = Modifier.size(24.dp) - ) - } - } - }, - content = { - ProfilesChipBar( - mainScreenController = mainScreenController, - onClickAddProfile = onClickAddProfile, - onClickChangeProfileName = onClickChangeProfileName, - tooltipBounds = tooltipBounds, - toolTipBoundsRequired = toolTipBoundsRequired - ) - } - ) -} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt index 9c3b4954..55b54ecb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenNavigationScreens.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/navigation/MainScreenNavigationScreens.kt @@ -16,17 +16,17 @@ * */ -package de.gematik.ti.erp.app.mainscreen.ui +package de.gematik.ti.erp.app.mainscreen.navigation import android.os.Parcelable import androidx.navigation.NavType import androidx.navigation.navArgument -import kotlinx.serialization.Serializable import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.card.model.command.UnlockMethod import de.gematik.ti.erp.app.prescription.detail.ui.model.PopUpName import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable @Parcelize @Serializable diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt index b43f786a..b50b6d6f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/presentation/MainScreenController.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.mainscreen.ui +package de.gematik.ti.erp.app.mainscreen.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -27,6 +27,7 @@ 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 de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -59,8 +60,8 @@ class MainScreenController( fun hasUnreadPrescriptionAvailable(profileIdentifier: ProfileIdentifier) = messageUseCase.unreadPrescriptionAvailable(profileIdentifier) - fun unreadOrders(profileIdentifier: ProfileIdentifier) = - messageUseCase.unreadOrders(profileIdentifier) + fun unreadOrders(profile: ProfilesUseCaseData.Profile) = + messageUseCase.unreadOrders(profile) fun unreadPrescriptionsInAllOrders(profileIdentifier: ProfileIdentifier) = messageUseCase.unreadPrescriptionsInAllOrders(profileIdentifier) diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt index 45730fa3..eb129be4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/FastTrackComponents.kt @@ -21,14 +21,14 @@ package de.gematik.ti.erp.app.mainscreen.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.usecase.IdpUseCase import de.gematik.ti.erp.app.utils.compose.AcceptDialog import io.github.aakira.napier.Napier diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt index e804bdd9..36d4b6f9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/InsecureDeviceScreen.kt @@ -55,8 +55,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt new file mode 100644 index 00000000..6b98b792 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomBar.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material.Badge +import androidx.compose.material.BadgedBox +import androidx.compose.material.BottomNavigationItem +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.PinDrop +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.ShoppingBag +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenBottomNavigationItems +import de.gematik.ti.erp.app.mainscreen.navigation.calculatePrescriptionCount +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.BottomNavigation + +private const val BottomBarBadgeOffsetX = -5 +private const val BottomBarBadgeOffsetY = 5 + +@Suppress("LongMethod") +@Composable +internal fun MainScreenBottomBar( + navController: NavController, + bottomNavController: NavController, + profilesController: ProfilesController, + mainScreenController: MainScreenController +) { + val navBackStackEntry by bottomNavController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + val activeProfile by profilesController.getActiveProfileState() + + var unreadPrescriptionCount = calculatePrescriptionCount(mainScreenController) + + val unreadOrdersCount by mainScreenController.unreadOrders(activeProfile).collectAsStateWithLifecycle(0) + + BottomNavigation( + backgroundColor = MaterialTheme.colors.surface, + extraContent = {} + ) { + MainScreenBottomNavigationItems.forEach { screen -> + BottomNavigationItem( + modifier = Modifier.testTag( + when (screen) { + MainNavigationScreens.Prescriptions -> TestTag.BottomNavigation.PrescriptionButton + MainNavigationScreens.Orders -> TestTag.BottomNavigation.OrdersButton + MainNavigationScreens.Pharmacies -> TestTag.BottomNavigation.PharmaciesButton + MainNavigationScreens.Settings -> TestTag.BottomNavigation.SettingsButton + else -> "" + } + ), + selectedContentColor = AppTheme.colors.primary700, + unselectedContentColor = AppTheme.colors.neutral600, + icon = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + when (screen) { + MainNavigationScreens.Prescriptions -> + if (unreadPrescriptionCount > 0) { + BadgedBox( + badge = { + Badge( + modifier = Modifier.offset( + x = BottomBarBadgeOffsetX.dp, + y = BottomBarBadgeOffsetY.dp + ), + backgroundColor = Color.Red, + contentColor = Color.White + ) { Text(unreadPrescriptionCount.toString()) } + } + ) { + Icon( + painterResource(R.drawable.ic_logo_outlined), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } else { + Icon( + painterResource(R.drawable.ic_logo_outlined), + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + MainNavigationScreens.Pharmacies -> Icon( + Icons.Outlined.PinDrop, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + + MainNavigationScreens.Orders -> + if (unreadOrdersCount > 0) { + BadgedBox( + badge = { + Badge( + modifier = Modifier.offset( + x = BottomBarBadgeOffsetX.dp, + y = BottomBarBadgeOffsetY.dp + ), + backgroundColor = Color.Red, + contentColor = Color.White + ) { Text(unreadOrdersCount.toString()) } + } + ) { + Icon( + Icons.Outlined.ShoppingBag, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } else { + Icon( + Icons.Outlined.ShoppingBag, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + + MainNavigationScreens.Settings -> Icon( + Icons.Outlined.Settings, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } + }, + label = { + Text( + stringResource( + when (screen) { + MainNavigationScreens.Prescriptions -> R.string.pres_bottombar_prescriptions + MainNavigationScreens.Orders -> R.string.pres_bottombar_orders + MainNavigationScreens.Pharmacies -> R.string.pres_bottombar_pharmacies + MainNavigationScreens.Settings -> R.string.main_settings_acc + else -> R.string.pres_bottombar_prescriptions + } + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + selected = currentRoute == screen.route, + alwaysShowLabel = true, + onClick = { + if (currentRoute != screen.route) { + when (screen.route) { + MainNavigationScreens.Pharmacies.route -> + navController.navigate(screen.path()) + + else -> + bottomNavController.navigate(screen.path()) + } + } + } + ) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt similarity index 70% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt index 78c0e25a..a0849658 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenBottomSheetContentState.kt @@ -26,10 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable @@ -40,27 +36,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType 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.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenBottomPopUpNames import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController import de.gematik.ti.erp.app.profiles.ui.AvatarPicker import de.gematik.ti.erp.app.profiles.ui.ColorPicker -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.profiles.ui.ProfileImage -import de.gematik.ti.erp.app.profiles.ui.ProfilesController -import de.gematik.ti.erp.app.profiles.ui.rememberProfilesController import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import de.gematik.ti.erp.app.theme.AppTheme @@ -70,7 +61,6 @@ import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -import de.gematik.ti.erp.app.utils.sanitizeProfileName import kotlinx.coroutines.launch @Stable @@ -98,22 +88,25 @@ sealed class MainScreenBottomSheetContentState { @Composable fun MainScreenBottomSheetContentState( - infoContentState: MainScreenBottomSheetContentState?, mainNavController: NavController, + profilesController: ProfilesController, + infoContentState: MainScreenBottomSheetContentState?, profileToRename: ProfilesUseCaseData.Profile, onCancel: () -> Unit ) { - val profilesController = rememberProfilesController() val settingsController = rememberSettingsController() - val profileHandler = LocalProfileHandler.current + val profile by profilesController.getActiveProfileState() val title = when (infoContentState) { is MainScreenBottomSheetContentState.EditProfilePicture -> stringResource(R.string.mainscreen_bottom_sheet_edit_profile_image) + is MainScreenBottomSheetContentState.EditProfileName -> stringResource(R.string.bottom_sheet_edit_profile_name_title) + is MainScreenBottomSheetContentState.AddProfile -> stringResource(R.string.bottom_sheet_edit_profile_name_title) + else -> null } @@ -140,27 +133,27 @@ fun MainScreenBottomSheetContentState( when (it) { is MainScreenBottomSheetContentState.EditProfilePicture -> EditProfileAvatar( - profile = profileHandler.activeProfile, + profile = profile, clearPersonalizedImage = { scope.launch { - profilesController.clearPersonalizedImage(profileHandler.activeProfile.id) + profilesController.clearPersonalizedImage(profile.id) } }, onPickPersonalizedImage = { mainNavController.navigate( MainNavigationScreens.ProfileImageCropper.path( - profileId = profileHandler.activeProfile.id + profileId = profile.id ) ) }, onSelectAvatar = { avatar -> scope.launch { - profilesController.saveAvatarFigure(profileHandler.activeProfile.id, avatar) + profilesController.saveAvatarFigure(profile.id, avatar) } }, onSelectProfileColor = { color -> scope.launch { - profilesController.updateProfileColor(profileHandler.activeProfile, color) + profilesController.updateProfileColor(profile, color) } } ) @@ -180,6 +173,7 @@ fun MainScreenBottomSheetContentState( profileToEdit = null, onCancel = onCancel ) + is MainScreenBottomSheetContentState.Welcome -> ConnectBottomSheetContent( onClickConnect = { @@ -187,7 +181,7 @@ fun MainScreenBottomSheetContentState( settingsController.welcomeDrawerShown() } mainNavController.navigate( - MainNavigationScreens.CardWall.path(profileHandler.activeProfile.id) + MainNavigationScreens.CardWall.path(profile.id) ) }, onCancel = { @@ -204,91 +198,12 @@ fun MainScreenBottomSheetContentState( } } -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun ProfileSheetContent( - profilesController: ProfilesController, - profileToEdit: ProfilesUseCaseData.Profile?, - addProfile: Boolean = false, - onCancel: () -> Unit -) { - val keyboardController = LocalSoftwareKeyboardController.current - val scope = rememberCoroutineScope() - val profilesState by profilesController.profilesState - var textValue by remember { mutableStateOf(profileToEdit?.name ?: "") } - var duplicated by remember { mutableStateOf(false) } - - val onEdit = { - if (!addProfile) { - profileToEdit?.let { - scope.launch { profilesController.updateProfileName(it.id, textValue) } - } - } else { - scope.launch { - profilesController.addProfile(textValue) - } - } - onCancel() - keyboardController?.hide() - } - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - OutlinedTextField( - modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField), - shape = RoundedCornerShape(8.dp), - value = textValue, - singleLine = true, - onValueChange = { - val isNotExistingText = textValue.trim() != profileToEdit?.name - val isNotExistingName = profilesState.containsProfileWithName(textValue) - val name = it.trimStart().sanitizeProfileName() - textValue = name - duplicated = isNotExistingText && isNotExistingName - }, - keyboardOptions = KeyboardOptions( - autoCorrect = true, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Done, - capitalization = KeyboardCapitalization.Sentences - ), - keyboardActions = KeyboardActions { - if (!duplicated && textValue.isNotEmpty()) { - onEdit() - } - }, - placeholder = { Text(stringResource(R.string.profile_edit_name_place_holder)) }, - isError = duplicated - ) - - if (duplicated) { - Text( - stringResource(R.string.edit_profile_duplicated_profile_name), - color = AppTheme.colors.red600, - style = AppTheme.typography.caption1, - modifier = Modifier.padding(start = PaddingDefaults.Medium) - ) - } - SpacerLarge() - PrimaryButton( - modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton), - enabled = !duplicated && textValue.isNotEmpty(), - onClick = { - onEdit() - } - ) { - Text(stringResource(R.string.profile_bottom_sheet_save)) - } - } -} - @Composable private fun EditProfileAvatar( profile: ProfilesUseCaseData.Profile, clearPersonalizedImage: () -> Unit, onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.AvatarFigure) -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit, onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit ) { ProfileColorAndImagePickerContent( @@ -305,27 +220,29 @@ private fun ProfileColorAndImagePickerContent( profile: ProfilesUseCaseData.Profile, clearPersonalizedImage: () -> Unit, onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.AvatarFigure) -> Unit, + onSelectAvatar: (ProfilesData.Avatar) -> Unit, onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit ) { - Column( - modifier = Modifier - .fillMaxSize() - ) { + var editableProfile by remember { mutableStateOf(profile) } + Column(modifier = Modifier.fillMaxSize()) { SpacerMedium() - ProfileImage(profile) { + ProfileImage(editableProfile) { + editableProfile = editableProfile.copy(image = null) clearPersonalizedImage() } SpacerXXLarge() AvatarPicker( - profile = profile, - currentAvatarFigure = profile.avatarFigure, + profile = editableProfile, + currentAvatar = editableProfile.avatar, onPickPersonalizedImage = onPickPersonalizedImage, - onSelectAvatar = onSelectAvatar + onSelectAvatar = { + editableProfile = editableProfile.copy(avatar = it) + onSelectAvatar(it) + } ) - if (profile.avatarFigure != ProfilesData.AvatarFigure.PersonalizedImage) { + if (editableProfile.avatar != ProfilesData.Avatar.PersonalizedImage) { SpacerXXLarge() SpacerMedium() Text( @@ -334,7 +251,13 @@ private fun ProfileColorAndImagePickerContent( ) SpacerLarge() - ColorPicker(profile.color, onSelectProfileColor) + ColorPicker( + profileColorName = editableProfile.color, + onSelectProfileColor = { + editableProfile = editableProfile.copy(color = it) + onSelectProfileColor(it) + } + ) SpacerLarge() } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt new file mode 100644 index 00000000..31d798f7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffold.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Scaffold +import androidx.compose.material.ScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.testTag +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.navigation.MainScreenContentNavHost +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.settings.ui.SettingsController + +@Composable +internal fun MainScreenScaffold( + mainScreenController: MainScreenController, + settingsController: SettingsController, + profilesController: ProfilesController, + mainNavController: NavController, + bottomNavController: NavHostController, + tooltipBounds: MutableState>, + onClickAddProfile: () -> Unit, + onClickChangeProfileName: (ProfilesUseCaseData.Profile) -> Unit, + onClickAvatar: () -> Unit, + scaffoldState: ScaffoldState +) { + val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) + val activeProfile by profilesController.getActiveProfileState() + + val isInPrescriptionScreen by remember { + derivedStateOf { + currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route + } + } + var topBarElevated by remember { mutableStateOf(true) } + + Scaffold( + modifier = Modifier.testTag(TestTag.Main.MainScreen), + topBar = { + if (currentBottomNavigationRoute?.destination?.route != MainNavigationScreens.Settings.route) { + MultiProfileTopAppBar( + mainScreenController = mainScreenController, + settingsController = settingsController, + profilesController = profilesController, + navController = mainNavController, + isInPrescriptionScreen = isInPrescriptionScreen, + tooltipBounds = tooltipBounds, + elevated = topBarElevated, + onClickAddProfile = onClickAddProfile, + onClickChangeProfileName = onClickChangeProfileName + ) + } + }, + bottomBar = { + MainScreenBottomBar( + navController = mainNavController, + mainScreenController = mainScreenController, + profilesController = profilesController, + bottomNavController = bottomNavController + ) + }, + floatingActionButton = { + if (isInPrescriptionScreen) { + RedeemFloatingActionButton( + activeProfile = activeProfile, + onClick = { + mainNavController.navigate(MainNavigationScreens.Redeem.path()) + } + ) + } + }, + scaffoldState = scaffoldState + ) { innerPadding -> + + MainScreenContentNavHost( + modifier = Modifier.padding(innerPadding), + mainScreenController = mainScreenController, + settingsController = settingsController, + mainNavController = mainNavController, + bottomNavController = bottomNavController, + onClickAvatar = onClickAvatar, + onElevateTopBar = { + topBarElevated = it + }, + onClickArchive = { mainNavController.navigate(MainNavigationScreens.Archive.path()) } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt new file mode 100644 index 00000000..2bfe8954 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenScaffoldContainer.kt @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import android.net.Uri +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.trackMainScreenBottomPopUps +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry +import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController.Companion.DEFAULT_EMPTY_PROFILE +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController +import de.gematik.ti.erp.app.settings.ui.rememberSettingsController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalMaterialApi::class) +@Suppress("LongMethod") +@Composable +internal fun MainScreenScaffoldContainer( + mainNavController: NavController, + onDeviceIsInsecure: () -> Unit +) { + val mainScreenController = rememberMainScreenController() + val settingsController = rememberSettingsController() + val profilesController = rememberProfilesController() + + val context = LocalContext.current + val bottomNavController = rememberNavController() + val currentBottomNavigationRoute by bottomNavController.currentBackStackEntryFlow.collectAsStateWithLifecycle(null) + var previousNavEntry by remember { mutableStateOf("main") } + trackNavigationChanges(bottomNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + val isInPrescriptionScreen by remember { + derivedStateOf { + currentBottomNavigationRoute?.destination?.route == MainNavigationScreens.Prescriptions.route + } + } + @Requirement( + "O.Plat_1#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check for insecure Devices on MainScreen." + ) + CheckInsecureDevice(onDeviceIsInsecure) + @Requirement( + "O.Arch_6#2", + "O.Resi_2#2", + "O.Resi_3#2", + "O.Resi_4#2", + "O.Resi_5#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Check device integrity." + ) + CheckDeviceIntegrity(mainScreenController, mainNavController) + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + MainScreenSnackbar( + mainScreenController = mainScreenController, + scaffoldState = scaffoldState + ) + OrderSuccessHandler(mainScreenController) + var mainScreenBottomSheetContentState: MainScreenBottomSheetContentState? by remember { mutableStateOf(null) } + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + skipHalfExpanded = true + ) + if (sheetState.currentValue != ModalBottomSheetValue.Hidden) { + DisposableEffect(Unit) { + onDispose { + scope.launch { + settingsController.welcomeDrawerShown() + } + } + } + } + LaunchedEffect(mainScreenBottomSheetContentState) { + if (mainScreenBottomSheetContentState != null) { + sheetState.show() + } else { + sheetState.hide() + } + } + val analytics = LocalAnalytics.current + val analyticsState by analytics.screenState + LaunchedEffect(sheetState.isVisible) { + if (sheetState.isVisible) { + mainScreenBottomSheetContentState?.let { analytics.trackMainScreenBottomPopUps(it) } + } else { + analytics.onPopUpClosed() + val route = Uri.parse(mainNavController.currentBackStackEntry!!.destination.route) + .buildUpon().clearQuery().build().toString() + trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + } + } + LaunchedEffect(Unit) { + if (settingsController.showWelcomeDrawer.first()) { + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.Welcome() + } + } + LaunchedEffect(sheetState.isVisible) { + if (sheetState.targetValue == ModalBottomSheetValue.Hidden) { + if (mainScreenBottomSheetContentState == MainScreenBottomSheetContentState.Welcome()) { + settingsController.welcomeDrawerShown() + } + mainScreenBottomSheetContentState = null + } + } + LaunchedEffect(Unit) { + if (settingsController.talkbackEnabled(context)) { + settingsController.mainScreenTooltipsShown() + } + } + var profileToRename by remember { + mutableStateOf(DEFAULT_EMPTY_PROFILE) + } + val toolTipBounds = remember { + mutableStateOf>(emptyMap()) + } + ToolTips(settingsController, isInPrescriptionScreen, toolTipBounds) + val coroutineScope = rememberCoroutineScope() + BackHandler(enabled = sheetState.isVisible) { + coroutineScope.launch { + sheetState.hide() + } + } + ModalBottomSheetLayout( + sheetState = sheetState, + modifier = Modifier + .imePadding() + .testTag(TestTag.Main.MainScreenBottomSheet.Modal), + sheetShape = remember { RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) }, + sheetContent = { + MainScreenBottomSheetContentState( + mainNavController = mainNavController, + profilesController = profilesController, + infoContentState = mainScreenBottomSheetContentState, + profileToRename = profileToRename, + onCancel = { + coroutineScope.launch { + sheetState.hide() + } + } + ) + } + ) { + // TODO: move to general place? + ExternalAuthenticationDialog() + MainScreenScaffold( + mainScreenController = mainScreenController, + settingsController = settingsController, + profilesController = profilesController, + mainNavController = mainNavController, + bottomNavController = bottomNavController, + tooltipBounds = toolTipBounds, + onClickAddProfile = { + mainScreenBottomSheetContentState = + MainScreenBottomSheetContentState.AddProfile() + }, + onClickChangeProfileName = { profile -> + profileToRename = profile + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfileName() + }, + onClickAvatar = { + mainScreenBottomSheetContentState = MainScreenBottomSheetContentState.EditProfilePicture() + }, + scaffoldState = scaffoldState + ) + } +} + +@Composable +private fun CheckInsecureDevice(onDeviceIsInsecure: () -> Unit) { + val settingsController = rememberSettingsController() + LaunchedEffect(Unit) { + if (BuildConfig.DEBUG) { + return@LaunchedEffect + } + @Requirement( + "O.Plat_1#3", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Navigate to insecure Devices warning." + ) + ( + withContext(Dispatchers.Main) { + if (settingsController.showInsecureDevicePrompt.first()) { + onDeviceIsInsecure() + } + } + ) + } +} + +@Composable +private fun CheckDeviceIntegrity( + mainScreenController: MainScreenController, + mainNavController: NavController +) { + LaunchedEffect(Unit) { + if (BuildConfig.DEBUG) { + return@LaunchedEffect + } + if (mainScreenController.checkDeviceIntegrity().first()) { + withContext(Dispatchers.Main) { + mainNavController.navigate(MainNavigationScreens.IntegrityNotOkScreen.route) + navOptions { + launchSingleTop = true + popUpTo(MainNavigationScreens.IntegrityNotOkScreen.path()) { + inclusive = true + } + } + } + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt index e940f864..520fb442 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MainScreenSnackbar.kt @@ -27,7 +27,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.ui.RefreshedState diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt new file mode 100644 index 00000000..adc1a90c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/MultiProfileTopAppBar.kt @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AddCircle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import de.gematik.ti.erp.app.settings.ui.SettingsController +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.compose.TopAppBarWithContent +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * The top appbar of the actual main screen. + */ +@Composable +internal fun MultiProfileTopAppBar( + navController: NavController, + mainScreenController: MainScreenController, + settingsController: SettingsController, + profilesController: ProfilesController, + isInPrescriptionScreen: Boolean, + elevated: Boolean, + onClickAddProfile: () -> Unit, + onClickChangeProfileName: (profile: Profile) -> Unit, + tooltipBounds: MutableState> +) { + val profiles by profilesController.getProfilesState() + val activeProfile by profilesController.getActiveProfileState() + val accScan = stringResource(R.string.main_scan_acc) + val elevation = remember(elevated) { if (elevated) AppBarDefaults.TopAppBarElevation else 0.dp } + + val toolTipBoundsRequired by produceState(initialValue = false) { + settingsController.showMainScreenToolTips().collect { + value = it + } + } + + val scope = rememberCoroutineScope() + + TopAppBarWithContent( + title = { + MainScreenTopBarTitle(isInPrescriptionScreen) + }, + elevation = elevation, + backgroundColor = AppTheme.colors.neutral025, + actions = @Composable { + if (isInPrescriptionScreen) { + // data matrix code scanner + IconButton( + onClick = { + scope.launch { + if (settingsController.mlKitNotAccepted().first()) { + navController.navigate(MainNavigationScreens.MlKitIntroScreen.path()) + } else { + navController.navigate(MainNavigationScreens.Camera.path()) + } + } + }, + modifier = Modifier + .testTag("erx_btn_scn_prescription") + .semantics { contentDescription = accScan } + .onGloballyPositioned { coordinates -> + if (toolTipBoundsRequired) { + tooltipBounds.value += Pair(0, coordinates.boundsInRoot()) + } + } + ) { + Icon( + imageVector = Icons.Rounded.AddCircle, + contentDescription = null, + tint = AppTheme.colors.primary700, + modifier = Modifier.size(24.dp) + ) + } + } + }, + content = { + ProfilesChipBar( + mainScreenController = mainScreenController, + profiles = profiles, + activeProfile = activeProfile, + tooltipBounds = tooltipBounds, + toolTipBoundsRequired = toolTipBoundsRequired, + onClickChangeProfileName = onClickChangeProfileName, + onClickAddProfile = onClickAddProfile, + onClickChangeActiveProfile = { profile -> + profilesController.switchActiveProfile(profile.id) + } + ) + } + ) +} + +@Composable +private fun MainScreenTopBarTitle(isInPrescriptionScreen: Boolean) { + val text = if (isInPrescriptionScreen) { + stringResource(R.string.pres_bottombar_prescriptions) + } else { + stringResource(R.string.orders_title) + } + Text( + text = text, + style = AppTheme.typography.h5, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) +} + +@Composable +private fun ProfilesChipBar( + mainScreenController: MainScreenController, + profiles: List, + activeProfile: Profile, + tooltipBounds: MutableState>, + toolTipBoundsRequired: Boolean, + onClickChangeActiveProfile: (Profile) -> Unit, + onClickChangeProfileName: (profile: Profile) -> Unit, + onClickAddProfile: () -> Unit +) { + val rowState = rememberLazyListState() + + var indexOfActiveProfile by remember { mutableStateOf(0) } + + LaunchedEffect(indexOfActiveProfile) { + delay(timeMillis = 300L) + rowState.animateScrollToItem(indexOfActiveProfile) + } + + LazyRow( + state = rowState, + horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Small), + modifier = Modifier + .fillMaxWidth() + .padding(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Small), + verticalAlignment = Alignment.CenterVertically + ) { + item { + SpacerSmall() + } + profiles.forEachIndexed { index, profile -> + if (profile.id == activeProfile.id) { + indexOfActiveProfile = index + 1 + } + + item { + ProfileChip( + profile = profile, + mainScreenController = mainScreenController, + selected = profile.id == activeProfile.id, + onClickChip = onClickChangeActiveProfile, + onClickChangeProfileName = onClickChangeProfileName, + tooltipBounds = tooltipBounds, + toolTipBoundsRequired = toolTipBoundsRequired + ) + SpacerSmall() + } + } + item { + AddProfileChip( + onClickAddProfile = onClickAddProfile, + tooltipBounds = tooltipBounds, + toolTipBoundsRequired = toolTipBoundsRequired + ) + SpacerMedium() + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt index 6436a91a..8705b9df 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/OrderSuccessHandler.kt @@ -22,10 +22,11 @@ import android.app.Activity import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.res.stringResource import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import com.google.android.play.core.review.ReviewManagerFactory -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.utils.compose.AcceptDialog @Composable diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt index 6116bf16..15e9079a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileChips.kt @@ -63,14 +63,14 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.ui.rememberRefreshPrescriptionsController -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import kotlinx.coroutines.delay @@ -126,13 +126,13 @@ fun AddProfileChip( @OptIn(ExperimentalFoundationApi::class) @Composable fun ProfileChip( - profile: ProfilesUseCaseData.Profile, - selected: Boolean, + profile: Profile, mainScreenController: MainScreenController, - onClickChip: (ProfileIdentifier) -> Unit, - onClickChangeProfileName: (profile: ProfilesUseCaseData.Profile) -> Unit, tooltipBounds: MutableState>, - toolTipBoundsRequired: Boolean + selected: Boolean, + toolTipBoundsRequired: Boolean, + onClickChangeProfileName: (profile: Profile) -> Unit, + onClickChip: (Profile) -> Unit ) { val refreshPrescriptionsController = rememberRefreshPrescriptionsController(mainScreenController) @@ -202,7 +202,7 @@ fun ProfileChip( modifier = Modifier .clip(shape) .combinedClickable( - onClick = { onClickChip(profile.id) }, + onClick = { onClickChip(profile) }, onLongClick = { onClickChangeProfileName(profile) }, role = Role.Button ) @@ -255,7 +255,7 @@ fun ProfileChip( } @Composable -private fun ssoStatusColor(profile: ProfilesUseCaseData.Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = +private fun ssoStatusColor(profile: Profile, ssoTokenScope: IdpData.SingleSignOnTokenScope?) = when { ssoTokenScope?.token?.isValid() == true -> AppTheme.colors.green400 profile.lastAuthenticated != null -> AppTheme.colors.neutral400 diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt new file mode 100644 index 00000000..75133699 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ProfileSheetContent.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.mainscreen.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.PrimaryButton +import de.gematik.ti.erp.app.utils.compose.SpacerLarge +import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName +import kotlinx.coroutines.launch + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ProfileSheetContent( + profilesController: ProfilesController, + profileToEdit: ProfilesUseCaseData.Profile?, + addProfile: Boolean = false, + onCancel: () -> Unit +) { + val keyboardController = LocalSoftwareKeyboardController.current + val scope = rememberCoroutineScope() + val profilesState by profilesController.getProfilesState() + var textValue by remember { mutableStateOf(profileToEdit?.name ?: "") } + var duplicated by remember { mutableStateOf(false) } + + val onEdit = { + if (!addProfile) { + profileToEdit?.let { + scope.launch { profilesController.updateProfileName(it.id, textValue) } + } + } else { + scope.launch { + profilesController.addProfile(textValue) + } + } + onCancel() + keyboardController?.hide() + } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField), + shape = RoundedCornerShape(8.dp), + value = textValue, + singleLine = true, + onValueChange = { + val isNotExistingText = textValue.trim() != profileToEdit?.name + val isNotExistingName = profilesState.containsProfileWithName(textValue) + val name = it.trimStart().sanitizeProfileName() + textValue = name + duplicated = isNotExistingText && isNotExistingName + }, + keyboardOptions = KeyboardOptions( + autoCorrect = true, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + capitalization = KeyboardCapitalization.Sentences + ), + keyboardActions = KeyboardActions { + if (!duplicated && textValue.isNotEmpty()) { + onEdit() + } + }, + placeholder = { Text(stringResource(R.string.profile_edit_name_place_holder)) }, + isError = duplicated + ) + + if (duplicated) { + Text( + stringResource(R.string.edit_profile_duplicated_profile_name), + color = AppTheme.colors.red600, + style = AppTheme.typography.caption1, + modifier = Modifier.padding(start = PaddingDefaults.Medium) + ) + } + SpacerLarge() + PrimaryButton( + modifier = Modifier.testTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton), + enabled = !duplicated && textValue.isNotEmpty(), + onClick = { + onEdit() + } + ) { + Text(stringResource(R.string.profile_bottom_sheet_save)) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt similarity index 91% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt index f29fe34f..86440db5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RedeemButton.kt @@ -33,15 +33,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.redeem.ui.rememberRedeemController import de.gematik.ti.erp.app.utils.compose.AcceptDialog @Composable fun RedeemFloatingActionButton( + activeProfile: ProfilesUseCaseData.Profile, onClick: () -> Unit ) { - val redeemState = rememberRedeemController() + val redeemState = rememberRedeemController(activeProfile) val hasRedeemableTasks by redeemState.hasRedeemableTasks var showNoRedeemableDialog by remember { mutableStateOf(false) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt index 841fc1fa..5846056e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/RefreshScaffold.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefreshIndicator import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.prescription.ui.rememberRefreshPrescriptionsController import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.theme.AppTheme diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt index 884e1cda..56814fd2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/ToolTip.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/mainscreen/ui/TopBars.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt new file mode 100644 index 00000000..6658b416 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingAuthTab.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.onboarding.model + +import androidx.compose.runtime.Immutable +@Immutable +enum class OnboardingAuthTab(val index: Int) { + Password(1), Biometric(0) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt new file mode 100644 index 00000000..73b2e84d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/model/OnboardingSecureAppMethod.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.onboarding.model + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import de.gematik.ti.erp.app.settings.ui.checkPassword +import kotlinx.parcelize.Parcelize +@Immutable +sealed class OnboardingSecureAppMethod { + @Immutable + @Parcelize + data class Password(val password: String, val repeatedPassword: String, val score: Int) : + OnboardingSecureAppMethod(), + Parcelable { + val checkedPassword: String? + get() = + if (checkPassword(password, repeatedPassword, score)) { + password + } else { + null + } + } + + @Parcelize + object DeviceSecurity : OnboardingSecureAppMethod(), Parcelable + + @Parcelize + object None : OnboardingSecureAppMethod(), Parcelable +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt new file mode 100644 index 00000000..3a7795fc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/AnimatedContentTransitions.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.onboarding.ui + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Left +import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection.Companion.Right +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith + +fun AnimatedContentTransitionScope.fade() = fadeIn(tween(durationMillis = 770)) togetherWith + fadeOut(tween(durationMillis = 770)) +fun AnimatedContentTransitionScope.slideRight() = + slideIntoContainer(Right) togetherWith slideOutOfContainer(Right) +fun AnimatedContentTransitionScope.slideLeft() = slideIntoContainer(Left) togetherWith slideOutOfContainer(Left) +fun AnimatedContentTransitionScope.slideHorizontal() = slideInHorizontally { it } + fadeIn() togetherWith + slideOutHorizontally { it } + fadeOut() diff --git a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt index ac7aeb80..06200df2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingComponents.kt @@ -16,16 +16,13 @@ * */ +@file:Suppress("MaximumLineLength") + package de.gematik.ti.erp.app.onboarding.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background @@ -76,11 +73,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.ui.AllowAnalyticsScreen import de.gematik.ti.erp.app.settings.ui.AllowBiometryScreen @@ -211,8 +210,7 @@ fun OnboardingScreen( "O.Arch_8#5", "O.Plat_11#5", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display terms of use as part of the onboarding." + - "Webview containing local html without javascript" + rationale = "Display terms of use as part of the onboarding. Webview containing local html without javascript" // ktlint-disable max-line-length ) WebViewScreen( modifier = Modifier.testTag(TestTag.Onboarding.TermsOfUseScreen), @@ -229,8 +227,7 @@ fun OnboardingScreen( "O.Arch_8#6", "O.Plat_11#6", sourceSpecification = "BSI-eRp-ePA", - rationale = "Display data privacy as part of the onboarding. " + - "Webview containing local html without javascript." + rationale = "Display data privacy as part of the onboarding. Webview containing local html without javascript." // ktlint-disable max-line-length ) WebViewScreen( modifier = Modifier.testTag(TestTag.Onboarding.DataProtectionScreen), @@ -284,7 +281,7 @@ private fun OnboardingScreenWithScaffold( page = it } - if (BuildKonfig.INTERNAL) { + if (BuildKonfig.INTERNAL && BuildConfig.DEBUG) { SkipOnBoardingButton(onClick = { onSaveNewUser( false, @@ -317,23 +314,15 @@ private fun OnboardingPages( AnimatedContent( modifier = Modifier.fillMaxSize(), + label = "", targetState = page, transitionSpec = { + val isInitialStateWelcome = initialState == OnboardingPages.Welcome + val isTargetStatePageOne = targetState == OnboardingPages.pageOf(1) when { - initialState == OnboardingPages.Welcome && - targetState == OnboardingPages.pageOf(1) -> { - fadeIn(tween(durationMillis = 770)) togetherWith fadeOut(tween(durationMillis = 770)) - } - - initialState.index > targetState.index -> { - slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Right) togetherWith - slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Right) - } - - else -> { - slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Left) togetherWith - slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Left) - } + isInitialStateWelcome && isTargetStatePageOne -> fade() + initialState.index > targetState.index -> slideRight() + else -> slideLeft() } } ) { @@ -473,7 +462,9 @@ private fun OnboardingWelcome( painterResource(R.drawable.onboarding_boygrannygranpa), null, alignment = Alignment.BottomStart, - modifier = Modifier.fillMaxSize().offset(x = (-60).dp) + modifier = Modifier + .fillMaxSize() + .offset(x = (-60).dp) ) } } @@ -612,7 +603,9 @@ private fun OnboardingPageTerms( rationale = "Display data protection as part of the onboarding" ) SecondaryButton( - modifier = Modifier.fillMaxWidth().testTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton), + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton), onClick = { navController.navigate(OnboardingNavigationScreens.DataProtection.path()) } @@ -628,7 +621,9 @@ private fun OnboardingPageTerms( rationale = "Display terms of use as part of the onboarding" ) SecondaryButton( - modifier = Modifier.fillMaxWidth().testTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton), + modifier = Modifier + .fillMaxWidth() + .testTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton), onClick = { navController.navigate(OnboardingNavigationScreens.TermsOfUse.path()) } @@ -682,8 +677,7 @@ private fun AnalyticsToggle( "A_19980#2", "A_19981#2", sourceSpecification = "gemSpec_eRp_FdV", - rationale = "The user is informed and required to accept this information via the data protection statement. " + - "Related data and services are listed in sections 5." + rationale = "The user is informed and required to accept this information via the data protection statement. Related data and services are listed in sections 5." // ktlint-disable max-line-length ) @Composable private fun DataTermsToggle( diff --git a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingScaffold.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingAppAuthentication.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSecureApp.kt similarity index 81% rename from android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingAppAuthentication.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSecureApp.kt index 6e81104e..b233ece5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/onboarding/ui/OnboardingAppAuthentication.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/onboarding/ui/OnboardingSecureApp.kt @@ -18,15 +18,9 @@ package de.gematik.ti.erp.app.onboarding.ui -import android.os.Parcelable import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.SizeTransform -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.with import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -39,7 +33,6 @@ import androidx.compose.material.ContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -55,52 +48,24 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.mainscreen.ui.TextTabRow +import de.gematik.ti.erp.app.onboarding.model.OnboardingAuthTab +import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.settings.ui.ConfirmationPasswordTextField import de.gematik.ti.erp.app.settings.ui.PasswordStrength import de.gematik.ti.erp.app.settings.ui.PasswordTextField -import de.gematik.ti.erp.app.settings.ui.checkPassword import de.gematik.ti.erp.app.settings.ui.checkPasswordScore import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.visualTestTag -import kotlinx.parcelize.Parcelize private const val POS_OF_ANIMATED_CONTENT_ITEM = 3 -@Immutable -sealed class OnboardingSecureAppMethod { - @Immutable - @Parcelize - data class Password(val password: String, val repeatedPassword: String, val score: Int) : - OnboardingSecureAppMethod(), - Parcelable { - val checkedPassword: String? - get() = - if (checkPassword(password, repeatedPassword, score)) { - password - } else { - null - } - } - - @Parcelize - object DeviceSecurity : OnboardingSecureAppMethod(), Parcelable - - @Parcelize - object None : OnboardingSecureAppMethod(), Parcelable -} - -@Immutable -private enum class AuthTab(val index: Int) { - Password(1), Biometric(0) -} - @OptIn(ExperimentalAnimationApi::class) @Composable fun OnboardingSecureApp( @@ -110,7 +75,7 @@ fun OnboardingSecureApp( onOpenBiometricScreen: () -> Unit ) { val lazyListState = rememberLazyListState() - var selectedTab by remember { mutableStateOf(AuthTab.Biometric) } + var selectedTab by remember { mutableStateOf(OnboardingAuthTab.Biometric) } OnboardingScaffold( modifier = Modifier @@ -120,23 +85,26 @@ fun OnboardingSecureApp( bottomBar = { OnboardingBottomBar( info = when (selectedTab) { - AuthTab.Password -> null - AuthTab.Biometric -> stringResource(R.string.onboarding_auth_biometric_info) + OnboardingAuthTab.Password -> null + OnboardingAuthTab.Biometric -> stringResource(R.string.onboarding_auth_biometric_info) }, buttonText = when (selectedTab) { - AuthTab.Password -> stringResource(R.string.onboarding_bottom_button_save) - AuthTab.Biometric -> stringResource(R.string.onboarding_bottom_button_choose) + OnboardingAuthTab.Password -> stringResource(R.string.onboarding_bottom_button_save) + OnboardingAuthTab.Biometric -> stringResource(R.string.onboarding_bottom_button_choose) }, buttonEnabled = when (selectedTab) { - AuthTab.Password -> (secureMethod as? OnboardingSecureAppMethod.Password)?.checkedPassword != null - AuthTab.Biometric -> true + OnboardingAuthTab.Password -> (secureMethod as? OnboardingSecureAppMethod.Password) + ?.checkedPassword != null + + OnboardingAuthTab.Biometric -> true }, buttonModifier = Modifier.testTag(TestTag.Onboarding.NextButton), onButtonClick = { when (selectedTab) { - AuthTab.Password -> + OnboardingAuthTab.Password -> onNextPage() - AuthTab.Biometric -> + + OnboardingAuthTab.Biometric -> onOpenBiometricScreen() } } @@ -177,10 +145,11 @@ fun OnboardingSecureApp( onClick = { when (it) { 0 -> { - selectedTab = AuthTab.Biometric + selectedTab = OnboardingAuthTab.Biometric } + 1 -> { - selectedTab = AuthTab.Password + selectedTab = OnboardingAuthTab.Password } } onSecureMethodChange(OnboardingSecureAppMethod.None) @@ -197,21 +166,18 @@ fun OnboardingSecureApp( } item { AnimatedContent( + label = "AnimatedContent", targetState = selectedTab, transitionSpec = { if (targetState.index > initialState.index) { - slideInHorizontally { width -> width } + fadeIn() with - slideOutHorizontally { width -> -width } + fadeOut() + slideHorizontal() } else { - slideInHorizontally { height -> -height } + fadeIn() with - slideOutHorizontally { width -> width } + fadeOut() - }.using( - SizeTransform(clip = false) - ) + slideHorizontal() + }.using(SizeTransform(clip = false)) } ) { targetTab -> when (targetTab) { - AuthTab.Password -> { + OnboardingAuthTab.Password -> { PasswordAuthentication( secureMethod = secureMethod, lazyListState = lazyListState, @@ -219,7 +185,8 @@ fun OnboardingSecureApp( onNext = onNextPage ) } - AuthTab.Biometric -> { + + OnboardingAuthTab.Biometric -> { } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/OderHealthCardModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt index 97df8e6e..4605bbe4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderComponents.kt @@ -80,9 +80,9 @@ import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.AppTheme @@ -104,7 +104,7 @@ fun HealthCardContactOrderScreen( val navController = rememberNavController() var previousNavEntry by remember { mutableStateOf("contactInsuranceCompany") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val title = stringResource(R.string.health_insurance_search_page_title) val navigationMode by navController.navigationModeState(HealthCardOrderNavigationScreens.HealthCardOrder.route) diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt index 6b150e09..204e9e5f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/ui/HealthCardOrderState.kt @@ -20,8 +20,8 @@ package de.gematik.ti.erp.app.orderhealthcard.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.Route import de.gematik.ti.erp.app.orderhealthcard.usecase.HealthCardOrderUseCase import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData @@ -55,7 +55,9 @@ class HealthCardOrderState( val state @Composable - get() = healthCardOrderStateFlow.collectAsState(HealthCardOrderStateData.defaultHealthCardOrderState) + get() = healthCardOrderStateFlow.collectAsStateWithLifecycle( + HealthCardOrderStateData.defaultHealthCardOrderState + ) fun onSelectInsuranceCompany(company: HealthCardOrderUseCaseData.HealthInsuranceCompany) { selectedCompanyFlow.value = company diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt index 9696d7e2..f96eca84 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/HealthCardOrderUseCase.kt @@ -19,11 +19,10 @@ package de.gematik.ti.erp.app.orderhealthcard.usecase import android.content.Context -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.orderhealthcard.usecase.model.HealthCardOrderUseCaseData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import java.io.InputStream diff --git a/android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orderhealthcard/usecase/model/HealthInsuranceCompany.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/MessagesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt similarity index 77% rename from android/src/main/java/de/gematik/ti/erp/app/orders/MessagesModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt index 07282f14..88496d10 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/MessagesModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/MessagesModule.kt @@ -20,6 +20,7 @@ package de.gematik.ti.erp.app.orders import de.gematik.ti.erp.app.orders.repository.CommunicationLocalDataSource import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.orders.repository.DefaultCommunicationRepository import de.gematik.ti.erp.app.orders.repository.PharmacyCacheLocalDataSource import de.gematik.ti.erp.app.orders.repository.PharmacyCacheRemoteDataSource import de.gematik.ti.erp.app.orders.usecase.OrderUseCase @@ -31,6 +32,18 @@ val messagesModule = DI.Module("messagesModule") { bindProvider { PharmacyCacheLocalDataSource(instance()) } bindProvider { PharmacyCacheRemoteDataSource(instance()) } bindProvider { CommunicationLocalDataSource(instance()) } - bindProvider { CommunicationRepository(instance(), instance(), instance(), instance(), instance(), instance()) } bindProvider { OrderUseCase(instance(), instance()) } } + +val messageRepositoryModule = DI.Module("messageRepositoryModule", allowSilentOverride = true) { + bindProvider { + DefaultCommunicationRepository( + instance(), + instance(), + instance(), + instance(), + instance(), + instance() + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt index 98138f57..c2b3a187 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/MessageSheets.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt similarity index 90% rename from android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt index b34448c0..9c51daab 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderEmptyScreens.kt @@ -35,19 +35,19 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.ui.EmptyScreenHome import de.gematik.ti.erp.app.prescription.ui.HomeConnectedWithoutToken import de.gematik.ti.erp.app.prescription.ui.HomeConnectedWithoutTokenBiometrics import de.gematik.ti.erp.app.prescription.ui.HomeHealthCardDisconnected -import de.gematik.ti.erp.app.profiles.ui.ProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.ProfileConnectionState import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerSmall @Composable fun LazyItemScope.OrderEmptyScreen( - connectionState: ProfileHandler.ProfileConnectionState?, + connectionState: ProfileConnectionState?, onClickRefresh: () -> Unit ) { Box( @@ -57,17 +57,17 @@ fun LazyItemScope.OrderEmptyScreen( contentAlignment = Alignment.Center ) { when (connectionState) { - ProfileHandler.ProfileConnectionState.LoggedOut -> { + ProfileConnectionState.LoggedOut -> { HomeHealthCardDisconnected( onClickAction = onClickRefresh ) } - ProfileHandler.ProfileConnectionState.LoggedOutWithoutTokenBiometrics -> { + ProfileConnectionState.LoggedOutWithoutTokenBiometrics -> { HomeConnectedWithoutTokenBiometrics( onClickAction = onClickRefresh ) } - ProfileHandler.ProfileConnectionState.LoggedOutWithoutToken -> { + ProfileConnectionState.LoggedOutWithoutToken -> { HomeConnectedWithoutToken( onClickAction = onClickRefresh ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt similarity index 92% rename from android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt index 37e06cc9..76ca0ea1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/ui/OrderScreen.kt @@ -19,7 +19,9 @@ package de.gematik.ti.erp.app.orders.ui import android.net.Uri +import android.os.Build import androidx.activity.compose.BackHandler +import androidx.annotation.RequiresApi import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.background @@ -58,7 +60,6 @@ import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -84,22 +85,25 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.trackOrderPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold import de.gematik.ti.erp.app.orders.usecase.OrderUseCase import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.prescription.ui.UserNotAuthenticatedDialog +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription import de.gematik.ti.erp.app.prescriptionId +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController 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.ui.ProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.connectionState import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -133,13 +137,14 @@ fun OrderScreen( mainScreenController: MainScreenController, onElevateTopBar: (Boolean) -> Unit ) { - val profileHandler = LocalProfileHandler.current + val profileController = rememberProfilesController() + val activeProfile by profileController.getActiveProfileState() var showUserNotAuthenticatedDialog by remember { mutableStateOf(false) } val onShowCardWall = { mainNavController.navigate( - MainNavigationScreens.CardWall.path(profileHandler.activeProfile.id) + MainNavigationScreens.CardWall.path(activeProfile.id) ) } if (showUserNotAuthenticatedDialog) { @@ -150,13 +155,13 @@ fun OrderScreen( } RefreshScaffold( - profileId = profileHandler.activeProfile.id, + profileId = activeProfile.id, onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, mainScreenController = mainScreenController, onShowCardWall = onShowCardWall ) { onRefresh -> Orders( - profileHandler = profileHandler, + activeProfile = activeProfile, onClickOrder = { orderId -> mainNavController.navigate( MainNavigationScreens.Messages.path(orderId) @@ -279,7 +284,7 @@ class MessageState( val messages @Composable get() = messageFlow - .collectAsState(emptyList()) + .collectAsStateWithLifecycle(emptyList()) private val orderFlow = orderUseCase .order(orderId) @@ -288,7 +293,7 @@ class MessageState( val order @Composable get() = orderFlow - .collectAsState(null) + .collectAsStateWithLifecycle(null) suspend fun consumeAllMessages() { withContext(NonCancellable) { @@ -343,12 +348,9 @@ class OrderState( } } - // keep; implementation follows - val errorFlow = orderUseCase.pharmacyCacheError - val orders @Composable - get() = orderFlow.collectAsState(emptyList()) + get() = orderFlow.collectAsStateWithLifecycle(emptyList()) } @Composable @@ -363,13 +365,12 @@ fun rememberOrderState( @Composable private fun Orders( - profileHandler: ProfileHandler, + activeProfile: ProfilesUseCaseData.Profile, onClickOrder: (orderId: String) -> Unit, onClickRefresh: () -> Unit, onElevateTopBar: (Boolean) -> Unit ) { val listState = rememberLazyListState() - val activeProfile = profileHandler.activeProfile val orderState = rememberOrderState(activeProfile.id) val orders by orderState.orders @@ -415,7 +416,7 @@ private fun Orders( OrderState.States.NoOrders -> { item { - val connectionState = profileHandler.connectionState(activeProfile) + val connectionState = activeProfile.connectionState() OrderEmptyScreen(connectionState, onClickRefresh = onClickRefresh) } } @@ -492,6 +493,7 @@ fun PrescriptionLabel(count: Int) { } } +@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterialApi::class) @Composable private fun Messages( @@ -564,7 +566,7 @@ private fun Messages( Modifier.padding(PaddingDefaults.Medium), verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Small) ) { - it.medicationNames.forEachIndexed { index, med -> + it.prescriptions.forEachIndexed { index, prescription -> Surface( modifier = Modifier .testTag(TestTag.Orders.Details.PrescriptionListItem) @@ -582,8 +584,17 @@ private fun Messages( Modifier.padding(PaddingDefaults.Medium), verticalAlignment = Alignment.CenterVertically ) { + val titlePrepend = stringResource(R.string.pres_details_scanned_medication) + + val name = when (prescription) { + is Prescription.ScannedPrescription -> + prescription.name + ?: "$titlePrepend ${prescription.index}" + is Prescription.SyncedPrescription -> prescription.name ?: "" + else -> "" + } Text( - med, + name, style = AppTheme.typography.subtitle1, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -696,6 +707,7 @@ private fun ReplyMessage( } } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun DispenseMessage( order: OrderUseCaseData.Order, diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt index bc8ee708..f6b87a88 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/OrderUseCase.kt @@ -19,11 +19,14 @@ package de.gematik.ti.erp.app.orders.usecase import de.gematik.ti.erp.app.DispatchProvider -import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.orders.repository.CommunicationRepository +import de.gematik.ti.erp.app.orders.usecase.model.OrderUseCaseData import de.gematik.ti.erp.app.pharmacy.repository.model.CommunicationPayloadInbox -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.mapper.toPrescription +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -32,9 +35,8 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json import java.net.URI @OptIn(ExperimentalCoroutinesApi::class) @@ -42,8 +44,6 @@ class OrderUseCase( private val repository: CommunicationRepository, private val dispatchers: DispatchProvider ) { - val pharmacyCacheError = repository.pharmacyCacheError - fun orders(profileIdentifier: ProfileIdentifier): Flow> = combine( repository.loadFirstDispReqCommunications(profileIdentifier), @@ -73,15 +73,16 @@ class OrderUseCase( } private suspend fun dispReqCommunicationToOrder( - communication: SyncedTaskData.Communication, + communication: Communication, withMedicationNames: Boolean, pharmacyName: String? ): OrderUseCaseData.Order { val taskIds = repository.taskIdsByOrder(communication.orderId).first() val hasUnreadMessages = repository.hasUnreadPrescription(taskIds, communication.orderId).first() - val medicationNames = if (withMedicationNames) { + val prescriptions = if (withMedicationNames) { taskIds.map { - repository.loadPrescriptionName(it).first() ?: "" + repository.loadSyncedByTaskId(it).first()?.toPrescription() + ?: repository.loadScannedByTaskId(it).first()?.toPrescription() } } else { emptyList() @@ -92,7 +93,7 @@ class OrderUseCase( } return communication.toOrder( - medicationNames = medicationNames, + prescriptions = prescriptions, hasUnreadMessages = hasUnreadMessages, taskIds = taskIds, pharmacyName = pharmacyName @@ -110,15 +111,15 @@ class OrderUseCase( } fun unreadPrescriptionAvailable(profileId: ProfileIdentifier) = - repository.hasUnreadPrescription(profileId).flowOn(dispatchers.IO) + repository.hasUnreadPrescription(profileId).flowOn(dispatchers.io) - fun unreadOrders(profileId: ProfileIdentifier) = - repository.unreadOrders(profileId).flowOn(dispatchers.IO) + fun unreadOrders(profile: ProfilesUseCaseData.Profile) = + repository.unreadOrders(profile.id).flowOn(dispatchers.io) fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier) = - repository.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.IO) + repository.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.io) suspend fun consumeCommunication(communicationId: String) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { repository.setCommunicationStatus(communicationId, true) } } @@ -128,7 +129,7 @@ class OrderUseCase( } suspend fun consumeOrder(orderId: String) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { repository.loadDispReqCommunications(orderId).first().forEach { repository.setCommunicationStatus(it.communicationId, true) } @@ -141,8 +142,8 @@ private val lenientJson = Json { ignoreUnknownKeys = true } -fun SyncedTaskData.Communication.toOrder( - medicationNames: List, +fun Communication.toOrder( + prescriptions: List, hasUnreadMessages: Boolean, taskIds: List, pharmacyName: String? @@ -150,13 +151,13 @@ fun SyncedTaskData.Communication.toOrder( OrderUseCaseData.Order( orderId = orderId, taskIds = taskIds, - medicationNames = medicationNames, + prescriptions = prescriptions, sentOn = sentOn, pharmacy = OrderUseCaseData.Pharmacy(name = pharmacyName ?: "", id = this.recipient), hasUnreadMessages = hasUnreadMessages ) -fun SyncedTaskData.Communication.toMessage() = +fun Communication.toMessage() = payload?.let { try { val inbox = lenientJson.decodeFromString(it) diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt index 4adab963..ba6d3955 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/orders/usecase/model/OrderUseCaseData.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.orders.usecase.model +import de.gematik.ti.erp.app.prescription.usecase.model.Prescription import kotlinx.datetime.Instant object OrderUseCaseData { @@ -29,7 +30,7 @@ object OrderUseCaseData { data class Order( val orderId: String, val taskIds: List, - val medicationNames: List, + val prescriptions: List, val sentOn: Instant, val pharmacy: Pharmacy, val hasUnreadMessages: Boolean diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt similarity index 77% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt index 17422a9d..dba6f8a3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/PharmacyModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/di/PharmacyModule.kt @@ -16,13 +16,16 @@ * */ -package de.gematik.ti.erp.app.pharmacy +package de.gematik.ti.erp.app.pharmacy.di -import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyLocalDataSource +import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.PharmacyLocalDataSource import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRemoteDataSource -import de.gematik.ti.erp.app.pharmacy.repository.DefaultPharmacyRepository +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyDirectRedeemUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase @@ -33,11 +36,16 @@ import org.kodein.di.instance val pharmacyModule = DI.Module("pharmacyModule") { bindProvider { PharmacyRemoteDataSource(instance(), instance()) } - bindProvider { PharmacyLocalDataSource(instance()) } bindProvider { DefaultPharmacyRepository(instance(), instance(), instance()) } bindProvider { ShippingContactRepository(instance(), instance()) } bindProvider { PharmacyDirectRedeemUseCase(instance()) } bindProvider { PharmacyMapsUseCase(instance(), instance(), instance()) } bindProvider { PharmacySearchUseCase(instance(), instance(), instance(), instance(), instance()) } bindProvider { PharmacyOverviewUseCase(instance(), instance()) } + bindProvider { GetOverviewPharmaciesUseCase(instance()) } + bindProvider { GetOrderStateUseCase(instance(), instance(), instance()) } +} + +val pharmacyRepositoryModule = DI.Module("pharmacyRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultPharmacyLocalDataSource(instance()) } } diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt new file mode 100644 index 00000000..70fe2c6e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/mapper/PrescriptionOrderMapper.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.mapper + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData + +fun ScannedTaskData.ScannedTask.toOrder() = + PharmacyUseCaseData.PrescriptionOrder( + taskId = taskId, + accessCode = accessCode, + title = name, + index = index, + timestamp = scannedOn, + substitutionsAllowed = false + ) + +fun SyncedTaskData.SyncedTask.toOrder() = + PharmacyUseCaseData.PrescriptionOrder( + taskId = taskId, + accessCode = accessCode!!, // TODO: check, why we get here a nullable!! + title = medicationName(), + index = null, + timestamp = authoredOn, + substitutionsAllowed = false + ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt index 37797b1c..ca8a391b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyController.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.pharmacy.ui +package de.gematik.ti.erp.app.pharmacy.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt new file mode 100644 index 00000000..23292967 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacyOrderController.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.presentation + +import androidx.annotation.RestrictTo +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData +import de.gematik.ti.erp.app.pharmacy.usecase.GetOrderStateUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController.Companion.DEFAULT_EMPTY_PROFILE +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class PharmacyOrderController( + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val pharmacySearchUseCase: PharmacySearchUseCase, + private val getOrderStateUseCase: GetOrderStateUseCase, + private val scope: CoroutineScope +) { + private val activeProfile by lazy { + getActiveProfileUseCase().stateIn(scope, SharingStarted.WhileSubscribed(0, 0), DEFAULT_EMPTY_PROFILE) + } + + private val isDirectRedeemEnabled = activeProfile.mapNotNull { it.lastAuthenticated == null } + + private val orders by lazy { getOrderStateUseCase() } + + @OptIn(ExperimentalCoroutinesApi::class) + private val hasRedeemableOrders by lazy { + orders.map { it.orders.isNotEmpty() }.mapLatest { return@mapLatest it } + } + + private val prescription by lazy { orders.map { it.orders } } + + private val unSelectedPrescriptions: MutableStateFlow> = MutableStateFlow(emptyList()) + + private val updatedOrders = + combine( + unSelectedPrescriptions, + orders + ) { unSelectedPrescriptions, prescriptionOrder -> + prescriptionOrder.copy( + orders = prescriptionOrder.orders.filter { it.taskId !in unSelectedPrescriptions } + ) + } + + val activeProfileState + @Composable + get() = activeProfile.collectAsStateWithLifecycle() + + val isDirectRedeemEnabledState + @Composable + get() = isDirectRedeemEnabled.collectAsStateWithLifecycle(false) + + val hasRedeemableOrdersState + @Composable + get() = hasRedeemableOrders.collectAsStateWithLifecycle(false) + + val orderState + @Composable + get() = updatedOrders.collectAsStateWithLifecycle(PharmacyUseCaseData.OrderState.Empty) + + val prescriptionsState + @Composable + get() = prescription.collectAsStateWithLifecycle(emptyList()) + + var selectedPharmacy: PharmacyUseCaseData.Pharmacy? by mutableStateOf(null) + private set + + var selectedOrderOption: PharmacyScreenData.OrderOption? by mutableStateOf(null) + private set + + fun onSelectPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy, orderOption: PharmacyScreenData.OrderOption) { + selectedPharmacy = pharmacy + selectedOrderOption = orderOption + } + + fun onSelectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { + unSelectedPrescriptions.update { it - order.taskId } + } + + fun onDeselectPrescription(order: PharmacyUseCaseData.PrescriptionOrder) { + unSelectedPrescriptions.update { it + order.taskId } + } + + fun onSaveContact(contact: PharmacyUseCaseData.ShippingContact) { + scope.launch { + pharmacySearchUseCase.saveShippingContact(contact) + } + } + + fun onResetPharmacySelection() { + selectedPharmacy = null + selectedOrderOption = null + } + + fun onResetPrescriptionSelection() { + unSelectedPrescriptions.value = emptyList() + } + + @RestrictTo(RestrictTo.Scope.TESTS) + val updatedOrdersForTest = updatedOrders +} + +@Composable +fun rememberPharmacyOrderController(): PharmacyOrderController { + val getActiveProfileUseCase by rememberInstance() + val pharmacySearchUseCase by rememberInstance() + val getOrderStateUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + return remember { + PharmacyOrderController( + getActiveProfileUseCase = getActiveProfileUseCase, + pharmacySearchUseCase = pharmacySearchUseCase, + getOrderStateUseCase = getOrderStateUseCase, + scope = scope + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt similarity index 90% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt index e1e65765..d1d15b45 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/PharmacySearchController.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.pharmacy.ui +package de.gematik.ti.erp.app.pharmacy.presentation import android.Manifest import android.annotation.SuppressLint @@ -27,7 +27,6 @@ import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.filter @@ -47,6 +47,7 @@ import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService import de.gematik.ti.erp.app.fhir.model.Location import de.gematik.ti.erp.app.fhir.model.isOpenAt import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.usecase.GetOverviewPharmaciesUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyMapsUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacyOverviewUseCase import de.gematik.ti.erp.app.pharmacy.usecase.PharmacySearchUseCase @@ -61,7 +62,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -70,6 +70,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -86,15 +87,25 @@ private const val DefaultRadiusInMeter = 999 * 1000.0 @Stable class PharmacySearchController( private val context: Context, - private val mapsUseCase: PharmacyMapsUseCase, + private val getOverviewPharmaciesUseCase: GetOverviewPharmaciesUseCase, + private val pharmacyMapsUseCase: PharmacyMapsUseCase, private val pharmacyOverviewUseCase: PharmacyOverviewUseCase, - private val searchUseCase: PharmacySearchUseCase, - coroutineScope: CoroutineScope + private val pharmacySearchUseCase: PharmacySearchUseCase, + private val scope: CoroutineScope ) { + + private val overviewPharmacies by lazy { + getOverviewPharmaciesUseCase().stateIn(scope, SharingStarted.Lazily, emptyList()) + } + + val overviewPharmaciesState + @Composable + get() = overviewPharmacies.collectAsStateWithLifecycle() + @Stable sealed interface State : PrescriptionServiceState { @Stable - object Loading : State + data object Loading : State @Stable data class Pharmacies(val pharmacies: List) : State @@ -121,19 +132,19 @@ class PharmacySearchController( .flatMapLatest { searchData -> isLoading = true - searchUseCase.searchPharmacies(searchData) + pharmacySearchUseCase.searchPharmacies(searchData) .map { pagingData -> pagingData.map { it.updateDistanceForEnabledLocation(searchData.locationMode) } .filter { it.providesDeliveryService(searchData.filter.deliveryService) } .filter { it.hasOpeningHours(searchData.filter.openNow) } - }.cachedIn(coroutineScope) + }.cachedIn(scope) } .onEach { isLoading = false } .flowOn(Dispatchers.IO) .shareIn( - coroutineScope, + scope, SharingStarted.WhileSubscribed(), 1 ) @@ -149,7 +160,7 @@ class PharmacySearchController( flow { emit(State.Loading) - val pharmacies = mapsUseCase.searchPharmacies(searchData) + val pharmacies = pharmacyMapsUseCase.searchPharmacies(searchData) pharmacies .map { it.updateDistanceForEnabledLocation(searchData.locationMode) } @@ -234,22 +245,6 @@ class PharmacySearchController( } } } - - private val pharmacyOverviewFlow = combine( - pharmacyOverviewUseCase.favoritePharmacies(), - pharmacyOverviewUseCase.oftenUsedPharmacies() - ) { favorites, oftenUsed -> - favorites + oftenUsed.filterNot { oftenUsedPharmacy -> - favorites.any { - it.telematikId == oftenUsedPharmacy.telematikId - } - } - }.map { PharmacySearchStateData.PharmacySearchOverviewState(it) } - - val pharmacySearchOverviewState - @Composable - get() = pharmacyOverviewFlow.collectAsState(PharmacySearchStateData.defaultOverviewPharmacies) - suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { pharmacyOverviewUseCase.deleteOverviewPharmacy(overviewPharmacy) } @@ -325,14 +320,16 @@ fun rememberPharmacySearchController(): PharmacySearchController { val pharmacyMapsUseCase by rememberInstance() val pharmacySearchUseCase by rememberInstance() val pharmacyOverviewUseCase by rememberInstance() + val getOverviewPharmaciesUseCase by rememberInstance() val scope = rememberCoroutineScope() return remember { PharmacySearchController( context = context, - mapsUseCase = pharmacyMapsUseCase, - searchUseCase = pharmacySearchUseCase, + pharmacyMapsUseCase = pharmacyMapsUseCase, + pharmacySearchUseCase = pharmacySearchUseCase, pharmacyOverviewUseCase = pharmacyOverviewUseCase, - coroutineScope = scope + getOverviewPharmaciesUseCase = getOverviewPharmaciesUseCase, + scope = scope ) } } @@ -361,10 +358,6 @@ object PharmacySearchStateData { val overviewPharmacies: List ) - val defaultOverviewPharmacies = PharmacySearchOverviewState( - overviewPharmacies = listOf() - ) - val defaultSearchData = PharmacyUseCaseData.SearchData( name = "", filter = PharmacyUseCaseData.Filter(), diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt index e794bd4a..a26e5ab5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemPrescriptionsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/presentation/RedeemPrescriptionsController.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.pharmacy.ui +package de.gematik.ti.erp.app.pharmacy.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -40,6 +40,7 @@ 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 io.github.aakira.napier.Napier import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.cancellable @@ -64,7 +65,8 @@ class RedeemPrescriptionsController( private val authenticator: Authenticator ) { sealed interface State : PrescriptionServiceState { - class Ordered(val orderId: String, val results: Map) : State + class Ordered(val orderId: String, val results: Map) : + State sealed interface Success : State { object Ok : Success @@ -132,7 +134,7 @@ class RedeemPrescriptionsController( contact: PharmacyUseCaseData.ShippingContact ) = flow { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val certHolderList = pharmacyDirectRedeemUseCase.loadCertificates( pharmacy.id ).getOrNull() @@ -216,7 +218,7 @@ class RedeemPrescriptionsController( } }.map { results -> State.Ordered(orderId.toString(), results) - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) @Requirement( "GS-A_5542#3", @@ -232,7 +234,7 @@ class RedeemPrescriptionsController( contact: PharmacyUseCaseData.ShippingContact ) = flow { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val results = prescriptions .map { prescription -> async { @@ -251,6 +253,10 @@ class RedeemPrescriptionsController( } } .awaitAll() + .map { + Napier.d { "orders are $it" } + it + } .toMap() overviewUseCase.saveOrUpdateUsedPharmacies(pharmacy) @@ -283,6 +289,7 @@ class RedeemPrescriptionsController( emit(it) } }.map { results -> + Napier.d { "State.Ordered(orderId.toString(), results) ${State.Ordered(orderId.toString(), results)}" } State.Ordered(orderId.toString(), results) } .retryWithAuthenticator( @@ -294,7 +301,7 @@ class RedeemPrescriptionsController( // TODO: remove for better error handling emit(State.Error.Unknown) } - .flowOn(dispatchers.IO) + .flowOn(dispatchers.io) } @Composable diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt index 0c8d6be2..7dcdc5bb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/EditShippingContactScreen.kt @@ -51,8 +51,9 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.max -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.ui.model.addressSupplementInputField import de.gematik.ti.erp.app.pharmacy.ui.model.cityInputField @@ -86,12 +87,12 @@ const val PostalCodeLength = 5 @Suppress("LongMethod") @Composable fun EditShippingContactScreen( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, onBack: () -> Unit ) { val listState = rememberLazyListState() - val state by orderState.order + val state by orderState.orderState var contact by remember(state.contact) { mutableStateOf(state.contact) } @@ -288,6 +289,7 @@ fun isPhoneValid(telephoneNumber: String, optional: Boolean): Boolean { private const val LayoutDelay = 330L +// TODO: Move to a different place, used in many places @OptIn(ExperimentalLayoutApi::class) fun Modifier.scrollOnFocus(to: Int, listState: LazyListState, offset: Int = 0) = composed { val coroutineScope = rememberCoroutineScope() diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt index 8444b30e..5f37cc3c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/FavoriteStarButton.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.TertiaryButton diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt index 8dd930a3..76f99961 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Favorites.kt @@ -47,8 +47,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -75,6 +76,7 @@ private sealed interface RefreshState { @Composable fun FavoritePharmacyCard( + modifier: Modifier = Modifier, overviewPharmacy: OverviewPharmacyData.OverviewPharmacy, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, pharmacySearchController: PharmacySearchController @@ -131,6 +133,7 @@ fun FavoritePharmacyCard( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) + .then(modifier) .clickable(role = Role.Button) { when (state) { is RefreshState.Success -> onSelectPharmacy((state as RefreshState.Success).pharmacy) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt index a770c3fd..26f3a8a4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsOverview.kt @@ -102,13 +102,17 @@ import com.google.maps.android.compose.MapUiSettings import com.google.maps.android.compose.Marker import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.rememberMarkerState -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics import de.gematik.ti.erp.app.core.complexAutoSaver +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.fhir.model.Location +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController +import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions +import de.gematik.ti.erp.app.pharmacy.presentation.queryNativeLocation import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacySearchPopUpNames import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData @@ -190,7 +194,7 @@ sealed interface PharmacySearchSheetContentState { @Composable fun MapsOverview( searchController: PharmacySearchController, - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, navController: NavHostController, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, onBack: () -> Unit @@ -258,7 +262,7 @@ fun MapsOverview( Box { ScaffoldWithMap( scaffoldState = scaffoldState, - orderState = orderState, + pharmacyOrderController = orderState, cameraPositionState = cameraPositionState, pharmacySearchController = searchController, pharmacies = pharmacies, @@ -301,7 +305,7 @@ fun MapsOverview( is PharmacySearchSheetContentState.PharmacySelected -> PharmacyBottomSheetDetails( - orderState = orderState, + orderController = orderState, pharmacy = (sheetState.content as PharmacySearchSheetContentState.PharmacySelected).pharmacy, onClickOrder = { pharmacy, orderOption -> @@ -372,7 +376,7 @@ fun rememberPharmacySheetState( @Composable private fun ScaffoldWithMap( scaffoldState: ScaffoldState, - orderState: PharmacyOrderState, + pharmacyOrderController: PharmacyOrderController, cameraPositionState: CameraPositionState, pharmacySearchController: PharmacySearchController, pharmacies: List, @@ -437,7 +441,7 @@ private fun ScaffoldWithMap( ) { innerPadding -> Box { FullscreenMap( - orderState = orderState, + orderState = pharmacyOrderController, cameraPositionState = cameraPositionState, innerPadding = innerPadding, pharmacies = pharmacies, @@ -565,7 +569,7 @@ private fun CameraAnimation( @Composable private fun FullscreenMap( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, cameraPositionState: CameraPositionState, innerPadding: PaddingValues, pharmacies: List, diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt index 4f9cc614..7d9f7e03 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/MapsSnackbar.kt @@ -19,7 +19,7 @@ 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.features.R import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt similarity index 85% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt index 39e4742a..f2433e1d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/Navigation.kt @@ -30,9 +30,14 @@ import androidx.compose.runtime.setValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController +import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyNavigationScreens -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.mainscreen.ui.rememberMainScreenController 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 @@ -42,22 +47,22 @@ import kotlinx.coroutines.launch @Suppress("LongMethod") @Composable fun PharmacyNavigation( - orderState: PharmacyOrderState = rememberPharmacyOrderState(), + pharmacyOrderController: PharmacyOrderController = rememberPharmacyOrderController(), isNestedNavigation: Boolean = false, onBack: () -> Unit, onFinish: () -> Unit ) { val scope = rememberCoroutineScope() val pharmacySearchController = rememberPharmacySearchController() + val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState var searchFilter by remember(pharmacySearchController.searchState.filter) { mutableStateOf(pharmacySearchController.searchState.filter) } - val hasRedeemableTasks by orderState.hasRedeemableTasks + val hasRedeemableOrders by pharmacyOrderController.hasRedeemableOrdersState LaunchedEffect(Unit) { searchFilter = searchFilter.copy(directRedeem = false) - if (orderState.profile.lastAuthenticated == null && hasRedeemableTasks - ) { + if (directRedeemEnabled && hasRedeemableOrders) { searchFilter = searchFilter.copy(directRedeem = true) } } @@ -121,7 +126,7 @@ fun PharmacyNavigation( } var previousNavEntry by remember { mutableStateOf("pharmacySearch") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val handleSearchResultFn = { searchResult: PharmacySearchController.SearchQueryResult -> when (searchResult) { @@ -145,7 +150,7 @@ fun PharmacyNavigation( NavigationAnimation(mode = navigationMode) { PharmacyOverviewScreen( isNestedNavigation = isNestedNavigation, - orderState = orderState, + orderState = pharmacyOrderController, onBack = onBack, navController = navController, onFilterChange = { searchFilter = it }, @@ -171,7 +176,7 @@ fun PharmacyNavigation( }, onSelectPharmacy = { pharmacy, orderOption -> scope.launch(Dispatchers.Main) { - orderState.onSelectPharmacy(pharmacy, orderOption) + pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) } } @@ -181,11 +186,11 @@ fun PharmacyNavigation( composable(PharmacyNavigationScreens.List.route) { NavigationAnimation(mode = navigationMode) { PharmacySearchResultScreen( - orderState = orderState, + orderState = pharmacyOrderController, navController = navController, searchController = pharmacySearchController, onBack = { - orderState.onResetPharmacySelection() + pharmacyOrderController.onResetPharmacySelection() navController.navigate(PharmacyNavigationScreens.StartSearch.path()) }, onClickMaps = { @@ -201,7 +206,7 @@ fun PharmacyNavigation( }, onSelectPharmacy = { pharmacy, orderOption -> scope.launch(Dispatchers.Main) { - orderState.onSelectPharmacy(pharmacy, orderOption) + pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) } } @@ -213,15 +218,15 @@ fun PharmacyNavigation( NavigationAnimation(mode = navigationMode) { MapsOverview( searchController = pharmacySearchController, - orderState = orderState, + orderState = pharmacyOrderController, navController = navController, onBack = { - orderState.onResetPharmacySelection() + pharmacyOrderController.onResetPharmacySelection() navController.popBackStack() }, onSelectPharmacy = { pharmacy, orderOption -> scope.launch { - orderState.onSelectPharmacy(pharmacy, orderOption) + pharmacyOrderController.onSelectPharmacy(pharmacy, orderOption) navController.navigate(PharmacyNavigationScreens.OrderOverview.path()) } } @@ -232,7 +237,7 @@ fun PharmacyNavigation( NavigationAnimation(mode = navigationMode) { val mainScreenController = rememberMainScreenController() OrderOverview( - orderState = orderState, + orderState = pharmacyOrderController, onClickContacts = { navController.navigate(PharmacyNavigationScreens.EditShippingContact.path()) }, @@ -250,7 +255,7 @@ fun PharmacyNavigation( composable(PharmacyNavigationScreens.EditShippingContact.route) { NavigationAnimation(mode = navigationMode) { EditShippingContactScreen( - orderState = orderState, + orderState = pharmacyOrderController, onBack = { navController.popBackStack() } @@ -260,7 +265,7 @@ fun PharmacyNavigation( composable(PharmacyNavigationScreens.PrescriptionSelection.route) { NavigationAnimation(mode = navigationMode) { PrescriptionSelection( - orderState = orderState, + orderState = pharmacyOrderController, onFinishSelection = { navController.popBackStack() }, onBack = { navController.popBackStack() } ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt index 09c8b455..d6c48bdf 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderOverview.kt @@ -79,9 +79,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.RedeemPrescriptionsController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberRedeemPrescriptionsController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState @@ -108,13 +111,13 @@ private val TopBarColor = Color(0xffd6e9fb) @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable fun OrderOverview( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, onClickContacts: () -> Unit, onSelectPrescriptions: () -> Unit, onBack: () -> Unit, onFinish: (Boolean) -> Unit ) { - val order by orderState.order + val order by orderState.orderState val selectedPharmacy = remember { orderState.selectedPharmacy!! } val selectedOrderOption = remember { orderState.selectedOrderOption!! } @@ -193,7 +196,7 @@ fun OrderOverview( Column(Modifier.padding(horizontal = PaddingDefaults.Medium)) { Text(stringResource(R.string.pharmacy_order_prescriptions), style = AppTheme.typography.h6) SpacerMedium() - order.prescriptions.takeIf { it.isNotEmpty() }?.let { + order.orders.takeIf { it.isNotEmpty() }?.let { PrescriptionSelectionButton( prescriptions = it, onClick = onSelectPrescriptions @@ -222,7 +225,7 @@ fun OrderOverview( ) } RedeemButton( - orderState = orderState, + pharmacyOrderController = orderState, scaffoldState = scaffoldState, shippingContactCompleted = shippingContactCompleted, onFinish = onFinish @@ -233,7 +236,7 @@ fun OrderOverview( @Composable private fun RedeemButton( - orderState: PharmacyOrderState, + pharmacyOrderController: PharmacyOrderController, scaffoldState: ScaffoldState, shippingContactCompleted: Boolean, onFinish: (Boolean) -> Unit @@ -242,9 +245,17 @@ private fun RedeemButton( val scope = rememberCoroutineScope() val redeemController = rememberRedeemPrescriptionsController() - val order by orderState.order - val selectedPharmacy = remember { orderState.selectedPharmacy!! } - val selectedOrderOption = remember { orderState.selectedOrderOption!! } + val profile by pharmacyOrderController.activeProfileState + + val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState + + val order by pharmacyOrderController.orderState + + // TODO : Remove !! and refactor + val selectedPharmacy = remember { pharmacyOrderController.selectedPharmacy!! } + + // TODO : Remove !! and refactor + val selectedOrderOption = remember { pharmacyOrderController.selectedOrderOption!! } var uploadInProgress by remember { mutableStateOf(false) } @@ -256,7 +267,6 @@ private fun RedeemButton( PrescriptionRedeemAlertDialog( title = dialogTitle, description = dialogDescription, - showDialog = showDialog, onDismiss = { showDialog = false onFinish(true) @@ -279,10 +289,10 @@ private fun RedeemButton( uploadInProgress = true scope.launch { try { - val redeemState = if (orderState.profile.lastAuthenticated == null) { + val redeemState = if (directRedeemEnabled) { redeemController.orderPrescriptionsDirectly( orderId = UUID.randomUUID(), - prescriptions = order.prescriptions, + prescriptions = order.orders, redeemOption = selectedOrderOption, pharmacy = selectedPharmacy, contact = order.contact @@ -290,9 +300,9 @@ private fun RedeemButton( } else { redeemController .orderPrescriptions( - profileId = orderState.profile.id, + profileId = profile.id, orderId = UUID.randomUUID(), - prescriptions = order.prescriptions, + prescriptions = order.orders, redeemOption = selectedOrderOption, pharmacy = selectedPharmacy, contact = order.contact @@ -390,19 +400,16 @@ fun responseCodeMessagesMap(context: Context): Map> { fun PrescriptionRedeemAlertDialog( title: String, description: String, - showDialog: Boolean, onDismiss: () -> Unit ) { - if (showDialog) { - AcceptDialog( - header = title, - info = description, - onClickAccept = { - onDismiss() - }, - acceptText = stringResource(R.string.pharmacy_search_apovz_call_failed_accept) - ) - } + AcceptDialog( + header = title, + info = description, + onClickAccept = { + onDismiss() + }, + acceptText = stringResource(R.string.pharmacy_search_apovz_call_failed_accept) + ) } @Composable @@ -548,19 +555,19 @@ private fun PrescriptionSelectionButton( prescriptions: List, onClick: () -> Unit ) { - val scannedRxTxt = stringResource(R.string.pres_details_scanned_prescription) - val title = if (prescriptions.size > 1) { - stringResource(R.string.pharmacy_order_nr_of_prescriptions, prescriptions.size) - } else { - prescriptions.first().title ?: scannedRxTxt - } + val titlePrepend = stringResource(R.string.pres_details_scanned_medication) - val desc = remember(prescriptions) { - if (prescriptions.size > 1) { - prescriptions.joinToString { it.title ?: scannedRxTxt } - } else { - null - } + val (title, desc) = when (prescriptions.size) { + 1 -> + Pair( + prescriptions.first().title ?: "$titlePrepend ${prescriptions.first().index}", + null + ) + + else -> Pair( + stringResource(R.string.pharmacy_order_nr_of_prescriptions, prescriptions.size), + prescriptions.joinToString { it.title ?: "$titlePrepend ${it.index}" } + ) } FlatButton( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt similarity index 61% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt index ccc2dbb5..db5d99f7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/OrderSelection.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,119 +48,123 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.deliveryUrlNotEmpty +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isDeliveryWithoutContactUrls +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isOnlineServiceWithoutContactUrls +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.isPickupWithoutContactUrls +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.onlineUrlNotEmpty +import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderExtensions.pickupUrlNotEmpty import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData -import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.shortToast -import kotlinx.coroutines.launch private const val MAX_OPTIONS = 3 @Composable internal fun OrderSelection( - pharmacy: PharmacyUseCaseData.Pharmacy, - orderState: PharmacyOrderState, - onOrderClicked: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit + pharmacy: Pharmacy, + pharmacyOrderController: PharmacyOrderController, + onOrderClicked: (Pharmacy, PharmacyScreenData.OrderOption) -> Unit ) { - val scope = rememberCoroutineScope() - var directRedeemEnabled by remember { - mutableStateOf(false) - } - val directRedeemUrlsNotPresent = ( - pharmacy.contacts.pickUpUrl.isEmpty() && - pharmacy.contacts.deliveryUrl.isEmpty() && - pharmacy.contacts.onlineServiceUrl.isEmpty() - ) - - LaunchedEffect(Unit) { - scope.launch { - directRedeemEnabled = orderState.profile.lastAuthenticated == null - } - } - val directPickUpServiceAvailable = - directRedeemEnabled && pharmacy.contacts.pickUpUrl.isNotEmpty() - val pickUpServiceVisible = - pharmacy.contacts.pickUpUrl.isNotEmpty() || directRedeemUrlsNotPresent - val pickupServiceEnabled = directPickUpServiceAvailable || - !directRedeemEnabled && pharmacy.pickupServiceAvailable() - - val directDeliveryServiceAvailable = - directRedeemEnabled && pharmacy.contacts.deliveryUrl.isNotEmpty() - val deliveryServiceVisible = directDeliveryServiceAvailable || - pharmacy.contacts.deliveryUrl.isNotEmpty() || - (directRedeemUrlsNotPresent && pharmacy.deliveryServiceAvailable()) - val deliveryServiceEnabled = directDeliveryServiceAvailable || - !directRedeemEnabled && pharmacy.deliveryServiceAvailable() - - val directOnlineServiceAvailable = - directRedeemEnabled && pharmacy.contacts.onlineServiceUrl.isNotEmpty() - val mailDeliveryVisible = directOnlineServiceAvailable || - pharmacy.contacts.onlineServiceUrl.isNotEmpty() || - (directRedeemUrlsNotPresent && pharmacy.onlineServiceAvailable()) - val onlineServiceEnabled = directOnlineServiceAvailable || - !directRedeemEnabled && pharmacy.onlineServiceAvailable() - - val nrOfServices = remember(pickUpServiceVisible, deliveryServiceVisible, mailDeliveryVisible) { - listOf(pickUpServiceVisible, deliveryServiceVisible, mailDeliveryVisible).count { it } + val directRedeemEnabled by pharmacyOrderController.isDirectRedeemEnabledState + + val hasNoPickupContact = pharmacy.contacts.pickUpUrl.isEmpty() + val hasNoDeliveryContact = pharmacy.contacts.pickUpUrl.isEmpty() + val hasNoOnlineServiceContact = pharmacy.contacts.pickUpUrl.isEmpty() + val hasNoContacts = listOf(hasNoPickupContact, hasNoDeliveryContact, hasNoOnlineServiceContact) + + val hasNoContactUrls = hasNoContacts.all { it } + + // service availability checks + val pickUpServiceAvailable = directRedeemEnabled && pharmacy.pickupUrlNotEmpty() + val deliveryServiceAvailable = directRedeemEnabled && pharmacy.deliveryUrlNotEmpty() + val onlineServiceAvailable = directRedeemEnabled && pharmacy.onlineUrlNotEmpty() + + // visibility checks + val pickUpServiceVisible = pickUpServiceAvailable || + pharmacy.pickupUrlNotEmpty() || + pharmacy.isPickupWithoutContactUrls(hasNoContactUrls) + + val deliveryServiceVisible = deliveryServiceAvailable || + pharmacy.deliveryUrlNotEmpty() || + pharmacy.isDeliveryWithoutContactUrls(hasNoContactUrls) + + val onlineServiceVisible = onlineServiceAvailable || + pharmacy.onlineUrlNotEmpty() || + pharmacy.isOnlineServiceWithoutContactUrls(hasNoContactUrls) + + // enabled checks + val pickupServiceEnabled = pharmacy.pickupUrlNotEmpty() || + pickUpServiceAvailable || + !directRedeemEnabled && pharmacy.isPickupService + + val deliveryServiceEnabled = pharmacy.deliveryUrlNotEmpty() || + deliveryServiceAvailable || + !directRedeemEnabled && pharmacy.isDeliveryService + + val onlineServiceEnabled = pharmacy.onlineUrlNotEmpty() || + onlineServiceAvailable || + !directRedeemEnabled && pharmacy.isOnlineService + + val numberOfServices = remember(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible) { + listOf(pickUpServiceVisible, deliveryServiceVisible, onlineServiceVisible).count { it } } - val isSingle = nrOfServices == 1 - val isLarge = nrOfServices != MAX_OPTIONS Row( horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium), modifier = Modifier.height(IntrinsicSize.Min) ) { - val orderModifier = Modifier - .weight(weight = 0.5f) - .fillMaxHeight() + val modifier = Modifier.weight(weight = 0.5f).fillMaxHeight() + if (pickUpServiceVisible) { OrderButton( - modifier = orderModifier.testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), + modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton), isServiceEnabled = pickupServiceEnabled, + isLarge = numberOfServices != MAX_OPTIONS, + text = stringResource(R.string.pharmacy_order_opt_collect), + image = painterResource(R.drawable.pharmacy_small), onClick = { onOrderClicked( pharmacy, PharmacyScreenData.OrderOption.PickupService ) - }, - isLarge = isLarge, - text = stringResource(R.string.pharmacy_order_opt_collect), - image = painterResource(R.drawable.pharmacy_small) + } ) } if (deliveryServiceVisible) { OrderButton( - modifier = orderModifier.testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), + modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton), isServiceEnabled = deliveryServiceEnabled, + isLarge = numberOfServices != MAX_OPTIONS, + text = stringResource(R.string.pharmacy_order_opt_delivery), + image = painterResource(R.drawable.delivery_car_small), onClick = { onOrderClicked( pharmacy, PharmacyScreenData.OrderOption.CourierDelivery ) - }, - isLarge = isLarge, - text = stringResource(R.string.pharmacy_order_opt_delivery), - image = painterResource(R.drawable.delivery_car_small) + } ) } - if (mailDeliveryVisible) { + if (onlineServiceVisible) { OrderButton( - modifier = orderModifier - .testTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton), + modifier = modifier.testTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton), isServiceEnabled = onlineServiceEnabled, - onClick = { onOrderClicked(pharmacy, PharmacyScreenData.OrderOption.MailDelivery) }, - isLarge = isLarge, + isLarge = numberOfServices != MAX_OPTIONS, text = stringResource(R.string.pharmacy_order_opt_mail), - image = painterResource(R.drawable.truck_small) + image = painterResource(R.drawable.truck_small), + onClick = { onOrderClicked(pharmacy, PharmacyScreenData.OrderOption.MailDelivery) } ) } - if (isSingle) { + if (numberOfServices == 1) { Spacer(Modifier.weight(weight = 0.5f)) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt index bc3bcc82..9999b742 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyBottomSheetDetails.kt @@ -50,9 +50,11 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextOverflow -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.fhir.model.Location +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.theme.AppTheme @@ -69,7 +71,7 @@ import kotlinx.coroutines.launch @Composable fun PharmacyBottomSheetDetails( - orderState: PharmacyOrderState, + orderController: PharmacyOrderController, pharmacy: PharmacyUseCaseData.Pharmacy, pharmacyPortalUri: String = stringResource(R.string.pharmacy_detail_pharmacy_portal_uri), pharmacyPortalText: String = stringResource(R.string.pharmacy_detail_data_info_domain), @@ -88,7 +90,8 @@ fun PharmacyBottomSheetDetails( start = infoText.indexOf(pharmacyPortalText), end = infoText.indexOf(pharmacyPortalText) + pharmacyPortalText.length ) - val hasRedeemableTasks = orderState.hasRedeemableTasks + val hasRedeemableOrders by orderController.hasRedeemableOrdersState + var showNoRedeemableTasksDialog by remember { mutableStateOf(false) } if (showNoRedeemableTasksDialog) { @@ -158,10 +161,10 @@ fun PharmacyBottomSheetDetails( } SpacerXXLarge() OrderSelection( - orderState = orderState, + pharmacyOrderController = orderController, pharmacy = pharmacy, onOrderClicked = { pharmacy: PharmacyUseCaseData.Pharmacy, option: PharmacyScreenData.OrderOption -> - if (!hasRedeemableTasks.value) { + if (!hasRedeemableOrders) { showNoRedeemableTasksDialog = true } else { onClickOrder(pharmacy, option) diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt index e223901b..75e7cb50 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyContact.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.fhir.model.OpeningHours import de.gematik.ti.erp.app.fhir.model.isOpenToday import de.gematik.ti.erp.app.theme.AppTheme diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt new file mode 100644 index 00000000..ef586fe6 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyOrderExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.ui + +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy + +object PharmacyOrderExtensions { + internal fun Pharmacy.isPickupWithoutContactUrls(hasNoContactUrls: Boolean) = + hasNoContactUrls && isPickupService + internal fun Pharmacy.isDeliveryWithoutContactUrls(hasNoContactUrls: Boolean) = + hasNoContactUrls && isDeliveryService + internal fun Pharmacy.isOnlineServiceWithoutContactUrls(hasNoContactUrls: Boolean) = + hasNoContactUrls && isOnlineService + internal fun Pharmacy.pickupUrlEmpty() = contacts.pickUpUrl.isEmpty() + internal fun Pharmacy.pickupUrlNotEmpty() = contacts.pickUpUrl.isNotEmpty() + internal fun Pharmacy.deliveryUrlEmpty() = contacts.deliveryUrl.isEmpty() + internal fun Pharmacy.deliveryUrlNotEmpty() = contacts.deliveryUrl.isNotEmpty() + internal fun Pharmacy.onlineUrlEmpty() = contacts.onlineServiceUrl.isEmpty() + internal fun Pharmacy.onlineUrlNotEmpty() = contacts.onlineServiceUrl.isNotEmpty() +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt index 2953417b..766dba55 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacyResultCard.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.fhir.model.DeliveryPharmacyService import de.gematik.ti.erp.app.fhir.model.LocalPharmacyService import de.gematik.ti.erp.app.fhir.model.OnlinePharmacyService diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt index 63f988eb..c741282f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchOverview.kt @@ -19,7 +19,6 @@ package de.gematik.ti.erp.app.pharmacy.ui import android.net.Uri -import android.os.Build import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -40,7 +39,9 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi @@ -56,7 +57,6 @@ import androidx.compose.material.icons.rounded.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -73,34 +73,35 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import de.gematik.ti.erp.app.theme.AppTheme -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacySearchController import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.ModalBottomSheet import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall import de.gematik.ti.erp.app.utils.compose.SpacerLarge +import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall +import de.gematik.ti.erp.app.utils.extensions.isGooglePlayServiceAvailable +import kotlinx.coroutines.async import kotlinx.coroutines.launch -private const val LastUsedPharmaciesListLength = 5 - @OptIn(ExperimentalMaterialApi::class) @Composable fun PharmacyOverviewScreen( isNestedNavigation: Boolean, - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, navController: NavHostController, onBack: () -> Unit, onStartSearch: () -> Unit, @@ -116,13 +117,16 @@ fun PharmacyOverviewScreen( val analytics = LocalAnalytics.current val analyticsState by analytics.screenState LaunchedEffect(sheetState.isVisible) { - if (sheetState.isVisible) { - analytics.trackPharmacySearchPopUps(sheetState.content) - } else { - analytics.onPopUpClosed() - val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) - .buildUpon().clearQuery().build().toString() - trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + async { + if (sheetState.isVisible) { + analytics.trackPharmacySearchPopUps(sheetState.content) + } else { + analytics.onPopUpClosed() + navController.currentBackStackEntry?.destination?.route?.let { uri -> + val route = Uri.parse(uri).buildUpon().clearQuery().build().toString() + trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + } + } } } @@ -182,7 +186,7 @@ fun PharmacyOverviewScreen( is PharmacySearchSheetContentState.PharmacySelected -> PharmacyBottomSheetDetails( - orderState = orderState, + orderController = orderState, pharmacy = (sheetState.content as PharmacySearchSheetContentState.PharmacySelected) .pharmacy, onClickOrder = { pharmacy, orderOption -> @@ -207,20 +211,12 @@ private fun OverviewContent( onShowFilter: () -> Unit, onShowMaps: () -> Unit ) { - val pharmacySearchState by pharmacySearchController.pharmacySearchOverviewState - val overviewPharmacyList = pharmacySearchState.overviewPharmacies + val overviewPharmacies by pharmacySearchController.overviewPharmaciesState val contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom) .add(WindowInsets(top = PaddingDefaults.Medium, bottom = PaddingDefaults.Medium)).asPaddingValues() val context = LocalContext.current - val isGoogleApiAvailable by remember { - mutableStateOf( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS - ) - } LazyColumn( modifier = Modifier @@ -246,7 +242,7 @@ private fun OverviewContent( onStartSearch() } } - if (isGoogleApiAvailable) { + if (context.isGooglePlayServiceAvailable()) { item { MapsSection(onShowMaps = onShowMaps) } @@ -259,9 +255,9 @@ private fun OverviewContent( onStartSearch = onStartSearch ) } - item { + if (overviewPharmacies.isNotEmpty()) { OverviewPharmacies( - oftenUsedPharmacyList = overviewPharmacyList, + pharmacies = overviewPharmacies, onSelectPharmacy = onSelectPharmacy, pharmacySearchController = pharmacySearchController ) @@ -293,39 +289,36 @@ private fun MapsSection( ) } -@Composable -private fun OverviewPharmacies( - oftenUsedPharmacyList: List, +@Suppress("FunctionName") +private fun LazyListScope.OverviewPharmacies( + pharmacies: List, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit, pharmacySearchController: PharmacySearchController ) { - if (oftenUsedPharmacyList.isNotEmpty()) { - Column( + item { + Box( modifier = Modifier .fillMaxWidth() .padding(horizontal = PaddingDefaults.Medium) ) { Text( - stringResource(R.string.pharmacy_my_pharmacies_header), + text = stringResource(R.string.pharmacy_my_pharmacies_header), style = AppTheme.typography.subtitle1, - modifier = Modifier.padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium), + modifier = Modifier + .padding(top = PaddingDefaults.XXLarge, bottom = PaddingDefaults.Medium), textAlign = TextAlign.Start ) - val shortOftenUsedPharmacyList = remember(oftenUsedPharmacyList) { - oftenUsedPharmacyList.take(LastUsedPharmaciesListLength) - } - Column { - for (oftenUsedPharmacy in shortOftenUsedPharmacyList) { - FavoritePharmacyCard( - overviewPharmacy = oftenUsedPharmacy, - onSelectPharmacy = onSelectPharmacy, - pharmacySearchController = pharmacySearchController - ) - SpacerMedium() - } - } } } + items(pharmacies) { + FavoritePharmacyCard( + modifier = Modifier.padding(horizontal = PaddingDefaults.Medium), + overviewPharmacy = it, + onSelectPharmacy = onSelectPharmacy, + pharmacySearchController = pharmacySearchController + ) + SpacerMedium() + } } @Composable diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt index 45313731..9409995a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PharmacySearchScreen.kt @@ -100,14 +100,17 @@ import androidx.navigation.NavHostController import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemsIndexed +import androidx.paging.compose.itemKey import com.google.accompanist.flowlayout.FlowRow -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.analytics.trackPharmacySearchPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacySearchController +import de.gematik.ti.erp.app.pharmacy.presentation.locationPermissions import de.gematik.ti.erp.app.pharmacy.ui.model.PharmacyScreenData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.pharmacyId @@ -510,7 +513,7 @@ private fun ErrorRetryHandler( @OptIn(ExperimentalMaterialApi::class) @Composable fun PharmacySearchResultScreen( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, searchController: PharmacySearchController, navController: NavHostController, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy, PharmacyScreenData.OrderOption) -> Unit, @@ -701,7 +704,7 @@ fun PharmacySearchResultScreen( is PharmacySearchSheetContentState.PharmacySelected -> PharmacyBottomSheetDetails( - orderState = orderState, + orderController = orderState, pharmacy = (sheetState.content as PharmacySearchSheetContentState.PharmacySelected).pharmacy, onClickOrder = { pharmacy, orderOption -> @@ -739,7 +742,7 @@ private fun SearchResultContent( val errorSubtitle = stringResource(R.string.search_pharmacy_error_subtitle) val errorAction = stringResource(R.string.search_pharmacy_error_action) - val itemPaddingModifier = Modifier + val modifier = Modifier .fillMaxWidth() .padding(PaddingDefaults.Medium) val loadState = searchPagingItems.loadState @@ -807,14 +810,18 @@ private fun SearchResultContent( ) } } - itemsIndexed(searchPagingItems) { index, pharmacy -> + items( + count = searchPagingItems.itemCount, + key = searchPagingItems.itemKey { it.id } + ) { index -> + val pharmacy = searchPagingItems[index] if (pharmacy != null) { PharmacySearchResult( - itemPaddingModifier, - index, - searchPagingItems.itemCount, - pharmacy, - onSelectPharmacy + modifier = modifier, + count = searchPagingItems.itemCount, + index = index, + pharmacy = pharmacy, + onSelectPharmacy = onSelectPharmacy ) } } @@ -837,8 +844,8 @@ private fun SearchResultContent( @Composable fun PharmacySearchResult( modifier: Modifier, + count: Int, index: Int, - itemCount: Int, pharmacy: PharmacyUseCaseData.Pharmacy, onSelectPharmacy: (PharmacyUseCaseData.Pharmacy) -> Unit ) { @@ -853,7 +860,8 @@ fun PharmacySearchResult( ) { onSelectPharmacy(pharmacy) } - if (index < itemCount - 1) { + Divider(startIndent = PaddingDefaults.Medium) + if (index < count - 1) { Divider(startIndent = PaddingDefaults.Medium) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt similarity index 81% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt index 245cc020..4166d689 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/PrescriptionSelection.kt @@ -48,9 +48,9 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.utils.toFormattedDate +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.prescriptionId import de.gematik.ti.erp.app.prescriptionIds @@ -61,26 +61,26 @@ import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.PrimaryButtonLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.dateTimeShortText +import de.gematik.ti.erp.app.utils.extensions.dateTimeShortText @Composable fun PrescriptionSelection( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, showNextButton: Boolean = false, backIsFinish: Boolean = true, onFinishSelection: () -> Unit, onBack: () -> Unit ) { - val prescriptions by orderState.prescriptions - val order by orderState.order + val prescriptions by orderState.prescriptionsState + val order by orderState.orderState var showNoSelectedRxDialog by remember { mutableStateOf(false) } - BackHandler(backIsFinish && order.prescriptions.isEmpty()) { + BackHandler(backIsFinish && order.orders.isEmpty()) { showNoSelectedRxDialog = true } val onFinishFn = { - if (order.prescriptions.isEmpty()) { + if (order.orders.isEmpty()) { showNoSelectedRxDialog = true } else { onFinishSelection() @@ -116,14 +116,12 @@ fun PrescriptionSelection( }, state = listState ) { - val prescriptionsIndices = processPrescriptionsDayIndicesForSelection(prescriptions) prescriptions.forEach { prescription -> item(key = "prescription-${prescription.taskId}") { PrescriptionItem( modifier = Modifier, prescription = prescription, - index = prescriptionsIndices.getOrDefault(prescription.taskId, 1), - checked = remember(prescription, order) { prescription in order.prescriptions }, + checked = remember(prescription, order) { prescription in order.orders }, onCheckedChange = { if (it) { orderState.onSelectPrescription(prescription) @@ -144,32 +142,14 @@ fun PrescriptionSelection( } } -fun processPrescriptionsDayIndicesForSelection( - prescriptions: List -): Map { - var previousPrescription: PharmacyUseCaseData.PrescriptionOrder? = null - var dayIndex = 1 - val indexedPrescriptions = mutableMapOf() - - prescriptions.forEach { - val current = it.timestamp.toFormattedDate() - val prev = previousPrescription?.timestamp?.toFormattedDate() - if (current == prev) dayIndex++ else dayIndex = 1 - previousPrescription = it - indexedPrescriptions[it.taskId] = dayIndex - } - return indexedPrescriptions -} - @Composable private fun PrescriptionItem( modifier: Modifier, prescription: PharmacyUseCaseData.PrescriptionOrder, - index: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit ) { - val scannedRxTxt = stringResource(R.string.pres_details_scanned_prescription) + val titlePrepend = stringResource(R.string.pres_details_scanned_medication) Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -184,18 +164,15 @@ private fun PrescriptionItem( prescriptionId = prescription.taskId } ) { - val dt = remember(prescription) { dateTimeShortText(prescription.timestamp) } + val prescriptionDateTime = remember(prescription) { dateTimeShortText(prescription.timestamp) } Column(Modifier.weight(1f)) { Text( - prescription.title ?: if (index > 0) { - "$scannedRxTxt $index" - } else { - scannedRxTxt - }, + prescription.title + ?: "$titlePrepend ${prescription.index}", style = AppTheme.typography.body1 ) Text( - dt, + prescriptionDateTime, style = AppTheme.typography.body2l ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt index 2595de37..b160d813 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/RedeemErrorMessage.kt @@ -19,7 +19,8 @@ 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.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.RedeemPrescriptionsController import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/VideoContent.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt index 87d5d48b..be317137 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/ContactInputFields.kt @@ -24,7 +24,7 @@ import androidx.compose.material.Text import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.ui.scrollOnFocus import de.gematik.ti.erp.app.utils.compose.InputField diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/Navigation.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyScreenData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyScreenData.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyScreenData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/ui/model/PharmacyScreenData.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt new file mode 100644 index 00000000..654ee5c7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOrderStateUseCase.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.mapper.toOrder +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData +import de.gematik.ti.erp.app.pharmacy.model.shippingContact +import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.ShippingContact.Companion.EmptyShippingContact +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData.ScannedTask +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData.SyncedTask +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +/** + * Gets the activeProfile from the [profileRepository]. Then it gets redeemed (scanned and synced) tasks for this + * profile from the [prescriptionRepository] and converts them into [PharmacyUseCaseData.PrescriptionOrder]. + * + * Now it checks the [shippingContactRepository] for a shipping contact, if not present gets it from + * the [prescriptionRepository] and saves it into the [shippingContactRepository]. + * Finally it returns a [PharmacyUseCaseData.OrderState] with the orders and shippingContact. + */ +class GetOrderStateUseCase( + private val profileRepository: ProfileRepository, + private val prescriptionRepository: PrescriptionRepository, + private val shippingContactRepository: ShippingContactRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(): Flow = + profileRepository.activeProfile().flatMapLatest { profile -> + combine( + shippingContactRepository.shippingContact(), + getRedeemedSyncedTasks(profile.id), + getRedeemedScannedTasks(profile.id) + ) { contact, syncedTasks, scannedTasks -> + val updatedContact = when { + syncedTasks.isNotEmpty() && contact == null -> + syncedTasks.first().shippingContact().updateShippingContactRepo() + + else -> contact + } + val orders = syncedTasks.map { it.toOrder() } + scannedTasks.map { it.toOrder() } + val shippingContact = updatedContact?.toModel() ?: EmptyShippingContact + PharmacyUseCaseData.OrderState( + orders = orders, + contact = shippingContact + ) + }.flowOn(dispatcher) + }.flowOn(dispatcher) + + private suspend fun PharmacyData.ShippingContact.updateShippingContactRepo(): + PharmacyData.ShippingContact { + shippingContactRepository.saveShippingContact(this) + return this + } + + private fun getRedeemedSyncedTasks(id: ProfileIdentifier): Flow> = + prescriptionRepository.syncedTasks(id) + .mapNotNull { tasks -> + tasks.filter { it.redeemState().isRedeemable() } + }.flowOn(dispatcher) + + private fun getRedeemedScannedTasks(id: ProfileIdentifier): Flow> = + prescriptionRepository.scannedTasks(id) + .mapNotNull { tasks -> + tasks.filter { + it.isRedeemable() + it.communications.isEmpty() + } + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt new file mode 100644 index 00000000..609e3632 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/GetOverviewPharmaciesUseCase.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.usecase + +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn + +private const val LAST_USED_PHARMACIES_COUNT = 5 + +class GetOverviewPharmaciesUseCase( + private val repository: PharmacyRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow> { + val result = combine( + repository.loadOftenUsedPharmacies(), + repository.loadFavoritePharmacies() + ) { oftenUsedPharmacies, favouritePharmacies -> + (oftenUsedPharmacies + favouritePharmacies).filter { mixedPharmacy -> + val booleanResult = favouritePharmacies.any { it.telematikId == mixedPharmacy.telematikId } + booleanResult + }.distinctBy { it.telematikId } + .take(LAST_USED_PHARMACIES_COUNT) + }.flowOn(dispatcher) + return result + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt index c380df06..3382b794 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacySearchUseCase.kt @@ -33,7 +33,7 @@ import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.repository.ShippingContactRepository import de.gematik.ti.erp.app.pharmacy.usecase.mapper.PharmacyInitialResultsPerPage import de.gematik.ti.erp.app.pharmacy.usecase.mapper.PharmacyNextResultsPerPage -import de.gematik.ti.erp.app.pharmacy.usecase.mapper.mapToUseCasePharmacies +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository import de.gematik.ti.erp.app.prescription.repository.RemoteRedeemOption @@ -94,7 +94,7 @@ class PharmacySearchUseCase( return repository.searchPharmacies(name, filter) .map { LoadResult.Page( - data = it.pharmacies.mapToUseCasePharmacies(), + data = it.pharmacies.toModel(), nextKey = if (it.bundleResultCount == PharmacyInitialResultsPerPage) { PharmacyPagingKey( it.bundleId, @@ -122,7 +122,7 @@ class PharmacySearchUseCase( val prevKey = if (key.offset == 0) null else key.copy(offset = max(0, key.offset - count)) LoadResult.Page( - data = it.pharmacies.mapToUseCasePharmacies(), + data = it.pharmacies.toModel(), nextKey = nextKey, prevKey = prevKey, itemsBefore = if (prevKey != null) count else 0, @@ -161,7 +161,7 @@ class PharmacySearchUseCase( maxSize = PharmacyInitialResultsPerPage * 2 ), pagingSourceFactory = { PharmacyPagingSource(searchData) } - ).flow.flowOn(dispatchers.IO) + ).flow.flowOn(dispatchers.io) } fun prescriptionDetailsForOrdering( @@ -196,15 +196,17 @@ class PharmacySearchUseCase( PharmacyUseCaseData.PrescriptionOrder( taskId = task.taskId, accessCode = task.accessCode, - title = null, + title = task.name, + index = task.index, timestamp = task.scannedOn, substitutionsAllowed = false ) } + syncedTasks.map { task -> PharmacyUseCaseData.PrescriptionOrder( taskId = task.taskId, - accessCode = task.accessCode!!, + accessCode = task.accessCode!!, // TODO: check, why we get here a nullable!! title = task.medicationName(), + index = null, timestamp = task.authoredOn, substitutionsAllowed = false ) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt new file mode 100644 index 00000000..a096eb1c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/FileProviderAuthority.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv + +interface FileProviderAuthority { + fun getFilePath(): String +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/PkvModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/PkvModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/PkvModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt index 2f7dd6a8..719fed55 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ConsentController.kt @@ -24,23 +24,19 @@ 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.features.R import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceState import de.gematik.ti.erp.app.prescription.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 @@ -144,11 +140,11 @@ fun rememberConsentController(profile: ProfilesUseCaseData.Profile): ConsentCont val consentUseCase by rememberInstance() val authenticator = LocalAuthenticator.current - return remember(profile.id, profile.insuranceInformation.insuranceIdentifier) { + return remember(profile.id, profile.insurance.insuranceIdentifier) { ConsentController( context = context, profileId = profile.id, - insuranceIdentifier = profile.insuranceInformation.insuranceIdentifier, + insuranceIdentifier = profile.insurance.insuranceIdentifier, useCase = consentUseCase, authenticator = authenticator, dispatchers = dispatchers diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt index a048be33..ccaf2f00 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDetailsScreen.kt @@ -31,20 +31,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.invoice.model.currencyString +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LabeledText import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.compose.visualTestTag - -import de.gematik.ti.erp.app.invoice.model.currencyString -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.utils.compose.LabeledText import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.visualTestTag @Composable fun InvoiceDetailsScreen( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt index 89a8ddcb..96634e68 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceDialogues.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.pkv.ui import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt index fc063ecc..193fdaa9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceInformationScreen.kt @@ -43,21 +43,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import androidx.compose.ui.text.font.FontWeight import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate.joinMedicationInfo import de.gematik.ti.erp.app.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.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.visualTestTag - -import androidx.compose.ui.text.font.FontWeight - import de.gematik.ti.erp.app.utils.compose.LabeledText +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.visualTestTag @Composable fun InvoiceInformationScreen( diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt index 92737218..f2ee2089 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceLocalCorrectionScreen.kt @@ -39,17 +39,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.ForceBrightness import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.DataMatrix import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.createBitMatrix +import de.gematik.ti.erp.app.utils.extensions.forceBrightness @Composable fun InvoiceLocalCorrectionScreen( @@ -73,7 +73,7 @@ fun InvoiceLocalCorrectionScreen( } val activity = LocalActivity.current - activity.ForceBrightness() + activity.forceBrightness() AnimatedElevationScaffold( modifier = Modifier diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceOverviewScreen.kt new file mode 100644 index 00000000..a25ec5dc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceOverviewScreen.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pkv.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.invoice.model.InvoiceData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.LabeledText +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import de.gematik.ti.erp.app.utils.compose.TertiaryButton +import de.gematik.ti.erp.app.utils.compose.visualTestTag + +@Composable +fun InvoiceOverviewScreen( + selectedProfile: ProfilesUseCaseData.Profile, + taskId: String, + onBack: () -> Unit, + onClickShowMore: () -> Unit, + onClickSubmit: () -> Unit +) { + val listState = rememberLazyListState() + val scaffoldState = rememberScaffoldState() + val invoicesController = rememberInvoicesController(profileId = selectedProfile.id) + val invoice by produceState(null) { + invoicesController.detailState(taskId).collect { + value = it + } + } + // var showDeleteInvoiceAlert by remember { mutableStateOf(false) } + + AnimatedElevationScaffold( + modifier = Modifier + .imePadding() + .visualTestTag(TestTag.Profile.InvoicesDetailScreen), + topBarTitle = "", + navigationMode = NavigationBarMode.Back, + scaffoldState = scaffoldState, + bottomBar = { + invoice?.let { + InvoiceDetailBottomBar( + it.invoice.totalBruttoAmount, + onClickSubmit = onClickSubmit + ) + } + }, + listState = listState, + actions = { + Row { + // TODO: ?? + } + }, + onBack = onBack + ) { innerPadding -> + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding( + top = PaddingDefaults.Medium + innerPadding.calculateTopPadding(), + bottom = PaddingDefaults.Medium + innerPadding.calculateBottomPadding(), + start = PaddingDefaults.Medium, + end = PaddingDefaults.Medium + ), + state = listState, + verticalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) + ) { + invoice?.let { + item { + InvoiceMedicationHeader(it) + } + item { + LabeledText( + description = stringResource(R.string.invoice_prescribed_by), + content = it.practitioner.name + ) + } + item { + LabeledText( + description = stringResource(R.string.invoice_redeemed_in), + content = it.pharmacyOrganization.name + ) + } + item { + LabeledText( + description = stringResource(R.string.invoice_redeemed_on), + content = it.whenHandedOver?.formattedString() + ) + } + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + TertiaryButton(onClick = onClickShowMore) { + Text(text = stringResource(R.string.invoice_show_more)) + } + } + } + } + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt index 4859ce37..698886b2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoiceThreeDotMenu.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.SpacerSmall diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt index 69a1020f..797eade8 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesController.kt @@ -20,16 +20,16 @@ package de.gematik.ti.erp.app.pkv.ui import android.content.Context import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember -import de.gematik.ti.erp.app.R +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.api.ApiCallException import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator import de.gematik.ti.erp.app.core.LocalAuthenticator -import de.gematik.ti.erp.app.utils.asFhirTemporal +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.invoice.model.PkvHtmlTemplate import de.gematik.ti.erp.app.invoice.usecase.InvoiceUseCase +import de.gematik.ti.erp.app.pkv.FileProviderAuthority import de.gematik.ti.erp.app.pkv.usecase.createSharableFileInCache import de.gematik.ti.erp.app.pkv.usecase.sharePDFFile import de.gematik.ti.erp.app.pkv.usecase.writePDFAttachments @@ -41,6 +41,7 @@ import de.gematik.ti.erp.app.prescription.ui.RefreshedState 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.utils.asFhirTemporal import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.cancellable @@ -54,7 +55,8 @@ import java.net.HttpURLConnection class InvoicesController( profileId: ProfileIdentifier, private val invoiceUseCase: InvoiceUseCase, - private val authenticator: Authenticator + private val authenticator: Authenticator, + private val fileProviderAuthority: FileProviderAuthority ) { @@ -74,14 +76,14 @@ class InvoicesController( val state @Composable - get() = stateFlow.collectAsState(null) + get() = stateFlow.collectAsStateWithLifecycle(null) fun detailState(taskId: String): Flow = invoiceUseCase.invoiceById(taskId) val isRefreshing @Composable - get() = invoiceUseCase.refreshInProgress.collectAsState() + get() = invoiceUseCase.refreshInProgress.collectAsStateWithLifecycle() fun downloadInvoices( profileId: ProfileIdentifier @@ -97,7 +99,11 @@ class InvoicesController( .catchAndTransformRemoteExceptions() .flowOn(Dispatchers.IO) - suspend fun shareInvoicePDF(context: Context, invoice: InvoiceData.PKVInvoice) { + suspend fun shareInvoicePDF( + context: Context, + invoice: InvoiceData.PKVInvoice, + fileProviderAuthority: FileProviderAuthority + ) { val html = PkvHtmlTemplate.createHTML(invoice) val file = createSharableFileInCache(context, "invoices", "invoice") @@ -107,7 +113,7 @@ class InvoicesController( } val subject = invoice.medicationRequest.medication?.name() + "_" + invoice.timestamp.asFhirTemporal().formattedString() - sharePDFFile(context, file, subject) + sharePDFFile(context, file, subject, fileProviderAuthority) } suspend fun deleteInvoice( @@ -148,12 +154,14 @@ class InvoicesController( fun rememberInvoicesController(profileId: ProfileIdentifier): InvoicesController { val invoiceUseCase by rememberInstance() val authenticator = LocalAuthenticator.current + val fileProviderAuthority by rememberInstance() return remember { InvoicesController( profileId = profileId, invoiceUseCase = invoiceUseCase, - authenticator = authenticator + authenticator = authenticator, + fileProviderAuthority = fileProviderAuthority ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt index 289a1571..768f50cb 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/InvoicesScreen.kt @@ -77,11 +77,11 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.invoice.model.InvoiceData import de.gematik.ti.erp.app.invoice.model.currencyString -import de.gematik.ti.erp.app.mainscreen.ui.rememberMainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController 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 diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt index 43922955..ab9bb92c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/ui/ShareInformationScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box - import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -53,24 +52,24 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pkv.FileProviderAuthority +import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import de.gematik.ti.erp.app.utils.compose.visualTestTag -import kotlinx.coroutines.launch - -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.utils.compose.SecondaryButton import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerSmall - +import de.gematik.ti.erp.app.utils.compose.visualTestTag import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance @Composable fun ShareInformationScreen( @@ -84,6 +83,7 @@ fun ShareInformationScreen( val context = LocalContext.current var innerHeight by remember { mutableStateOf(0) } val listState = rememberLazyListState() + val fileProvider by rememberInstance() LaunchedEffect(listState) { listState.scrollToItem(listState.layoutInfo.totalItemsCount, 0) @@ -100,7 +100,7 @@ fun ShareInformationScreen( ShareInformationBottomBar { scope.launch { invoicesController.detailState(taskId).first()?.let { - invoicesController.shareInvoicePDF(context, it) + invoicesController.shareInvoicePDF(context, it, fileProvider) onBack() } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt similarity index 89% rename from android/src/main/java/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt index f2118dfc..a0b7341c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/pkv/usecase/CreatePdf.kt @@ -32,15 +32,17 @@ 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 de.gematik.ti.erp.app.BuildConfig +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.pkv.FileProviderAuthority import io.github.aakira.napier.Napier import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File -import java.util.* +import java.util.GregorianCalendar +import java.util.UUID import kotlin.coroutines.suspendCoroutine -private const val FileProviderAuthority = "${BuildConfig.APPLICATION_ID}.fileprovider" +private const val FileProviderAuthority = "${BuildConfig.LIBRARY_PACKAGE_NAME}.fileprovider" private const val PDFDensity = 600 private const val PDFMargin = 24 @@ -64,8 +66,14 @@ fun createSharableFileInCache(context: Context, path: String, filePrefix: String return newFile } -fun sharePDFFile(context: Context, file: File, subject: String) { - val uri = FileProvider.getUriForFile(context, FileProviderAuthority, file) +fun sharePDFFile( + context: Context, + file: File, + subject: String, + fileProviderAuthority: FileProviderAuthority +) { + val path = fileProviderAuthority.getFilePath() + val uri = FileProvider.getUriForFile(context, path, file) ShareCompat.IntentBuilder(context) .setType("application/pdf") diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt similarity index 68% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt index 3cc78828..1a0cbdb9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/PrescriptionModule.kt @@ -18,16 +18,19 @@ package de.gematik.ti.erp.app.prescription -import de.gematik.ti.erp.app.prescription.repository.LocalDataSource +import de.gematik.ti.erp.app.prescription.repository.DefaultPrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository -import de.gematik.ti.erp.app.prescription.repository.RemoteDataSource import de.gematik.ti.erp.app.prescription.ui.TwoDCodeProcessor import de.gematik.ti.erp.app.prescription.ui.TwoDCodeScanner import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator +import de.gematik.ti.erp.app.prescription.usecase.GeneratePrescriptionDetailsUseCase import de.gematik.ti.erp.app.prescription.usecase.GetActivePrescriptionsUseCase import de.gematik.ti.erp.app.prescription.usecase.GetArchivedPrescriptionsUseCase import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import de.gematik.ti.erp.app.prescription.usecase.RefreshPrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.UpdateScannedTaskNameUseCase import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.bindSingleton @@ -37,11 +40,16 @@ val prescriptionModule = DI.Module("prescriptionModule") { bindProvider { TwoDCodeProcessor() } bindProvider { TwoDCodeScanner(instance()) } bindProvider { TwoDCodeValidator() } - bindSingleton { LocalDataSource(instance()) } - bindSingleton { PrescriptionRepository(instance(), instance(), instance()) } - bindSingleton { RemoteDataSource(instance()) } + bindSingleton { PrescriptionLocalDataSource(instance()) } + bindSingleton { PrescriptionRemoteDataSource(instance()) } bindSingleton { PrescriptionUseCase(instance(), instance(), instance()) } bindSingleton { RefreshPrescriptionUseCase(instance(), instance(), instance()) } bindProvider { GetActivePrescriptionsUseCase(instance()) } bindProvider { GetArchivedPrescriptionsUseCase(instance()) } + bindProvider { UpdateScannedTaskNameUseCase(instance()) } + bindProvider { GeneratePrescriptionDetailsUseCase(instance()) } +} + +val prescriptionRepositoryModule = DI.Module("prescriptionRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultPrescriptionRepository(instance(), instance(), instance()) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt similarity index 85% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt index 3ab2e388..2236e6af 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/AccidentInformation.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -49,12 +49,15 @@ private const val NoInfo = "-" @Composable fun AccidentInformation( - prescription: PrescriptionData.Synced, + prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced + // TODO : UI for accident types - val isAccident = remember(prescription) { - prescription.medicationRequest.accidentType != SyncedTaskData.AccidentType.None + val isAccident = remember(syncedPrescription) { + syncedPrescription?.medicationRequest?.accidentType != SyncedTaskData.AccidentType.None } val listState = rememberLazyListState() @@ -82,9 +85,9 @@ fun AccidentInformation( } item { val text = if (isAccident) { - remember(LocalConfiguration.current, prescription.medicationRequest.dateOfAccident) { + remember(LocalConfiguration.current, syncedPrescription?.medicationRequest?.dateOfAccident) { val dtFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - prescription.medicationRequest.dateOfAccident + syncedPrescription?.medicationRequest?.dateOfAccident ?.toLocalDateTime(TimeZone.currentSystemDefault()) ?.date ?.toJavaLocalDate() @@ -101,7 +104,7 @@ fun AccidentInformation( } item { val text = if (isAccident) { - prescription.medicationRequest.location ?: MissingValue + syncedPrescription?.medicationRequest?.location ?: MissingValue } else { NoInfo } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeletePrescriptions.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt index 70155dc4..5fc8277d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DeleteSnackbar.kt @@ -19,7 +19,7 @@ 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.features.R import de.gematik.ti.erp.app.prescription.ui.GeneralErrorState import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DetailNavController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DetailNavController.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/DetailNavController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/DetailNavController.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt index b069e829..0f894d03 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/InfoSheet.kt @@ -38,14 +38,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsPopUpNames import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerSmall -import de.gematik.ti.erp.app.utils.dateTimeMediumText +import de.gematik.ti.erp.app.utils.extensions.dateTimeMediumText import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.days diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt index 4d6e77b2..fad04b93 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/IngredientScreen.kt @@ -33,7 +33,7 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.NavigationBarMode diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt similarity index 90% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt index 968a57b4..21770df6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/MedicationOverviewScreen.kt @@ -32,9 +32,10 @@ import androidx.compose.material.SnackbarHost import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -46,10 +47,11 @@ import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge @Composable fun MedicationOverviewScreen( - prescription: PrescriptionData.Synced, + prescriptionDetailsController: PrescriptionDetailsController, onClickMedication: (PrescriptionData.Medication) -> Unit, onBack: () -> Unit ) { + val prescription by prescriptionDetailsController.prescriptionState val scaffoldState = rememberScaffoldState() val listState = rememberLazyListState() @@ -62,7 +64,8 @@ fun MedicationOverviewScreen( snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, actions = {} ) { innerPadding -> - prescription.medicationRequest.medication?.let { med -> + val syncedPrescription = (prescription as? PrescriptionData.Synced) + syncedPrescription?.medicationRequest?.medication?.let { med -> LazyColumn( state = listState, modifier = Modifier @@ -82,7 +85,7 @@ fun MedicationOverviewScreen( text = med.name(), label = null, onClick = { - onClickMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) + onClickMedication(PrescriptionData.Medication.Request(syncedPrescription.medicationRequest)) } ) } @@ -96,7 +99,7 @@ fun MedicationOverviewScreen( SpacerMedium() } - prescription.medicationDispenses.forEach { dispense -> + syncedPrescription.medicationDispenses.forEach { dispense -> // TODO: add tracking event (with dispenseId + performer) in case of medication is null dispense.medication?.let { item { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt similarity index 84% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt index fec71584..39ef6a32 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/OrganizationScreen.kt @@ -28,11 +28,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label @@ -41,10 +42,13 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium @Composable fun OrganizationScreen( - prescription: PrescriptionData.Synced, + prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { - val organization = prescription.organization + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced + + val organization = syncedPrescription?.organization val noValueText = stringResource(R.string.pres_details_no_value) val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -66,35 +70,35 @@ fun OrganizationScreen( SpacerMedium() Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.Name), - text = organization.name ?: noValueText, + text = organization?.name ?: noValueText, label = stringResource(id = R.string.pres_detail_organization_label_name) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.Address), - text = organization.address?.joinToString()?.takeIf { it.isNotEmpty() } ?: noValueText, + text = organization?.address?.joinToString()?.takeIf { it.isNotEmpty() } ?: noValueText, label = stringResource(id = R.string.pres_detail_organization_label_address) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.BSNR), - text = organization.uniqueIdentifier ?: noValueText, + text = organization?.uniqueIdentifier ?: noValueText, label = stringResource(id = R.string.pres_detail_organization_label_id) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.Phone), - text = organization.phone ?: noValueText, + text = organization?.phone ?: noValueText, label = stringResource(id = R.string.pres_detail_organization_label_telephone) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Organization.EMail), - text = organization.mail ?: noValueText, + text = organization?.mail ?: noValueText, label = stringResource(id = R.string.pres_detail_organization_label_email) ) SpacerMedium() diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt similarity index 83% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt index 1a78efc5..fd9a347f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PatientScreen.kt @@ -28,14 +28,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.insuranceState import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.repository.statusMapping @@ -43,16 +44,18 @@ import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium -import de.gematik.ti.erp.app.utils.temporalText +import de.gematik.ti.erp.app.utils.extensions.temporalText import kotlinx.datetime.TimeZone @Composable fun PatientScreen( - prescription: PrescriptionData.Synced, + prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { - val patient = prescription.patient - val insurance = prescription.insurance + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced + val patient = syncedPrescription?.patient + val insurance = syncedPrescription?.insurance val noValueText = stringResource(R.string.pres_details_no_value) val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -74,21 +77,21 @@ fun PatientScreen( SpacerMedium() Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.Name), - text = patient.name ?: noValueText, + text = patient?.name ?: noValueText, label = stringResource(R.string.pres_detail_patient_label_name) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.KVNR), - text = patient.insuranceIdentifier ?: noValueText, + text = patient?.insuranceIdentifier ?: noValueText, label = stringResource(R.string.pres_detail_patient_label_insurance_id) ) } item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.Address), - text = patient.address?.joinToString()?.takeIf { it.isNotEmpty() } ?: noValueText, + text = patient?.address?.joinToString()?.takeIf { it.isNotEmpty() } ?: noValueText, label = stringResource(R.string.pres_detail_patient_label_address) ) } @@ -96,7 +99,7 @@ fun PatientScreen( Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.BirthDate), text = remember(LocalConfiguration.current, patient) { - patient.birthdate?.let { + patient?.birthdate?.let { temporalText(it, TimeZone.currentSystemDefault()) } ?: noValueText }, @@ -106,7 +109,7 @@ fun PatientScreen( item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.Patient.InsuranceName), - text = insurance.name ?: noValueText, + text = insurance?.name ?: noValueText, label = stringResource(R.string.pres_detail_patient_label_insurance) ) } @@ -115,9 +118,9 @@ fun PatientScreen( modifier = Modifier .testTag(TestTag.Prescriptions.Details.Patient.InsuranceState) .semantics { - insuranceState = insurance.status + insuranceState = insurance?.status }, - text = insurance.status?.let { statusMapping[it]?.let { stringResource(it) } } ?: noValueText, + text = insurance?.status?.let { statusMapping[it]?.let { stringResource(it) } } ?: noValueText, label = stringResource(R.string.pres_detail_patient_label_member_status) ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt similarity index 84% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt index 44e7d7f3..d9ad3e3f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriberScreen.kt @@ -28,9 +28,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label @@ -39,10 +40,13 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium @Composable fun PrescriberScreen( - prescription: PrescriptionData.Synced, + prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { - val practitioner = prescription.practitioner + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced + + val practitioner = syncedPrescription?.practitioner val noValueText = stringResource(R.string.pres_details_no_value) val listState = rememberLazyListState() AnimatedElevationScaffold( @@ -61,19 +65,19 @@ fun PrescriberScreen( item { SpacerMedium() Label( - text = practitioner.name ?: noValueText, + text = practitioner?.name ?: noValueText, label = stringResource(R.string.pres_detail_practitioner_label_name) ) } item { Label( - text = practitioner.qualification ?: noValueText, + text = practitioner?.qualification ?: noValueText, label = stringResource(R.string.pres_detail_practitioner_label_qualification) ) } item { Label( - text = practitioner.practitionerIdentifier ?: noValueText, + text = practitioner?.practitionerIdentifier ?: noValueText, label = stringResource(R.string.pres_detail_practitioner_label_id) ) SpacerMedium() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt new file mode 100644 index 00000000..e00829f4 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailScreen.kt @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT 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:OptIn(ExperimentalMaterialApi::class) + +package de.gematik.ti.erp.app.prescription.detail.ui + +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackPrescriptionDetailPopUps +import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry +import de.gematik.ti.erp.app.core.LocalAnalytics +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsNavigationScreens +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.ui.DirectAssignmentChip +import de.gematik.ti.erp.app.prescription.ui.FailureDetailsStatusChip +import de.gematik.ti.erp.app.prescription.ui.PrescriptionStateInfo +import de.gematik.ti.erp.app.prescription.ui.SubstitutionAllowedChip +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium +import de.gematik.ti.erp.app.utils.compose.SpacerXLarge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +const val MissingValue = "---" + +@Composable +fun PrescriptionDetailsScreen( + taskId: String, + mainNavController: NavController +) { + val prescriptionDetailsController = rememberPrescriptionDetailsController(taskId) + + var selectedMedication: PrescriptionData.Medication? by remember { mutableStateOf(null) } + var selectedIngredient: SyncedTaskData.Ingredient? by remember { mutableStateOf(null) } + + val mainScope = rememberCoroutineScope { Dispatchers.Main } + val onBack: () -> Unit = { + mainScope.launch { + mainNavController.popBackStack() // TODO onBack instead of NavController + } + } + val navController = rememberNavController() + var previousNavEntry by remember { mutableStateOf("prescriptionDetail") } + + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + + NavHost( + navController = navController, + startDestination = PrescriptionDetailsNavigationScreens.Overview.route + ) { + composable(PrescriptionDetailsNavigationScreens.Overview.route) { + PrescriptionDetailsWithScaffold( + prescriptionDetailsController = prescriptionDetailsController, + navController = navController, + onClickMedication = { + selectedMedication = it + navController.navigate(PrescriptionDetailsNavigationScreens.Medication.path()) + }, + onBack = onBack + ) + } + composable(PrescriptionDetailsNavigationScreens.MedicationOverview.route) { + MedicationOverviewScreen( + prescriptionDetailsController = prescriptionDetailsController, + onClickMedication = { + selectedMedication = it + navController.navigate(PrescriptionDetailsNavigationScreens.Medication.path()) + }, + onBack = onBack + ) + } + composable(PrescriptionDetailsNavigationScreens.Medication.route) { + SyncedMedicationDetailScreen( + prescriptionDetailsController = prescriptionDetailsController, + medication = requireNotNull(selectedMedication), + onClickIngredient = { + selectedIngredient = it + navController.navigate(PrescriptionDetailsNavigationScreens.Ingredient.path()) + }, + onBack = { + navController.popBackStack() + } + ) + } + composable(PrescriptionDetailsNavigationScreens.Ingredient.route) { + IngredientScreen( + ingredient = requireNotNull(selectedIngredient), + onBack = { + navController.popBackStack() + } + ) + } + composable(PrescriptionDetailsNavigationScreens.Patient.route) { + PatientScreen( + prescriptionDetailsController = prescriptionDetailsController, + onBack = { + navController.popBackStack() + } + ) + } + composable(PrescriptionDetailsNavigationScreens.Prescriber.route) { + PrescriberScreen( + prescriptionDetailsController = prescriptionDetailsController, + onBack = { + navController.popBackStack() + } + ) + } + composable(PrescriptionDetailsNavigationScreens.Accident.route) { + AccidentInformation( + prescriptionDetailsController = prescriptionDetailsController, + onBack = { navController.popBackStack() } + ) + } + composable(PrescriptionDetailsNavigationScreens.Organization.route) { + OrganizationScreen( + prescriptionDetailsController = prescriptionDetailsController, + onBack = { navController.popBackStack() } + ) + } + composable(PrescriptionDetailsNavigationScreens.TechnicalInformation.route) { + TechnicalInformation( + taskId = taskId, + prescriptionDetailsController = prescriptionDetailsController, + onBack = { navController.popBackStack() } + ) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun PrescriptionDetailsWithScaffold( + prescriptionDetailsController: PrescriptionDetailsController, + navController: NavHostController, + onClickMedication: (PrescriptionData.Medication) -> Unit, + onBack: () -> Unit +) { + val prescription by prescriptionDetailsController.prescriptionState + + val scaffoldState = rememberScaffoldState() + val listState = rememberLazyListState() + + val sheetState = rememberModalBottomSheetState( + ModalBottomSheetValue.Hidden, + confirmValueChange = { it != ModalBottomSheetValue.HalfExpanded } + ) + + val coroutineScope = rememberCoroutineScope() + var infoBottomSheetContent: PrescriptionDetailBottomSheetContent? by remember { mutableStateOf(null) } + + val analytics = LocalAnalytics.current + val analyticsState by analytics.screenState + LaunchedEffect(sheetState.isVisible) { + if (sheetState.isVisible) { + infoBottomSheetContent?.let { analytics.trackPrescriptionDetailPopUps(it) } + } else { + analytics.onPopUpClosed() + val route = Uri.parse(navController.currentBackStackEntry!!.destination.route) + .buildUpon().clearQuery().build().toString() + trackScreenUsingNavEntry(route, analytics, analyticsState.screenNamesList) + } + } + LaunchedEffect(infoBottomSheetContent) { + if (infoBottomSheetContent != null) { + sheetState.show() + } else { + sheetState.hide() + } + } + ModalBottomSheetLayout( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.Screen), + sheetState = sheetState, + sheetContent = { + Box( + Modifier + .heightIn(min = 56.dp) + .navigationBarsPadding() + ) { + infoBottomSheetContent?.let { + PrescriptionDetailInfoSheetContent(infoContent = it) + } + } + }, + sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) { + PrescriptionDetailsScaffold( + prescription = prescription, + scaffoldState = scaffoldState, + listState = listState, + prescriptionDetailsController = prescriptionDetailsController, + navController = navController, + onClickMedication = onClickMedication, + onChangeSheetContent = { + infoBottomSheetContent = it + coroutineScope.launch { + sheetState.show() + } + }, + onBack = onBack + ) + } +} + +@Composable +fun SyncedHeader( + prescription: PrescriptionData.Synced, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + prescription.name ?: stringResource(R.string.prescription_medication_default_name), + style = AppTheme.typography.h5, + textAlign = TextAlign.Center, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + when { + prescription.isIncomplete -> { + SpacerShortMedium() + FailureDetailsStatusChip( + onClick = { + onShowInfo(PrescriptionDetailBottomSheetContent.Failure()) + } + ) + } + + prescription.isDirectAssignment -> { + SpacerShortMedium() + DirectAssignmentChip( + onClick = { + onShowInfo( + PrescriptionDetailBottomSheetContent.DirectAssignment() + ) + } + ) + } + + prescription.isSubstitutionAllowed -> { + SpacerShortMedium() + SubstitutionAllowedChip( + onClick = { + onShowInfo( + PrescriptionDetailBottomSheetContent.SubstitutionAllowed() + ) + } + ) + } + } + + SpacerShortMedium() + + val onClick = when { + !prescription.isDirectAssignment && + ( + prescription.state is SyncedTaskData.SyncedTask.Ready || + prescription.state is SyncedTaskData.SyncedTask.LaterRedeemable + ) -> { + { + onShowInfo( + PrescriptionDetailBottomSheetContent.HowLongValid( + prescription + ) + ) + } + } + + else -> null + } + SyncedStatus( + prescription = prescription, + onClick = onClick + ) + SpacerXLarge() + } +} + +@Composable +fun SyncedStatus( + modifier: Modifier = Modifier, + prescription: PrescriptionData.Synced, + onClick: (() -> Unit)? = null +) { + val clickableModifier = if (onClick != null) { + Modifier + .clickable(role = Role.Button, onClick = onClick) + .padding(start = PaddingDefaults.Tiny) + } else { + Modifier + } + + Row( + modifier = modifier + .then(clickableModifier), + verticalAlignment = Alignment.CenterVertically + ) { + if (prescription.isDirectAssignment) { + val text = if (prescription.isDispensed) { + stringResource(R.string.pres_details_direct_assignment_received_state) + } else { + stringResource(R.string.pres_details_direct_assignment_state) + } + Text( + text, + style = AppTheme.typography.body2l, + textAlign = TextAlign.Center + ) + } else { + PrescriptionStateInfo(prescription.state, textAlign = TextAlign.Center) + } + if (onClick != null) { + Spacer(Modifier.padding(2.dp)) + Icon( + Icons.Rounded.KeyboardArrowRight, + null, + modifier = Modifier.size(16.dp), + tint = AppTheme.colors.primary600 + ) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt new file mode 100644 index 00000000..833b4334 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsController.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.prescription.usecase.GeneratePrescriptionDetailsUseCase +import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase +import de.gematik.ti.erp.app.prescription.usecase.UpdateScannedTaskNameUseCase +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +@Stable +class PrescriptionDetailsController( + private val taskId: String, + private val generatePrescriptionDetailsUseCase: GeneratePrescriptionDetailsUseCase, + private val prescriptionUseCase: PrescriptionUseCase, + private val updateScannedTaskNameUseCase: UpdateScannedTaskNameUseCase, + private val scope: CoroutineScope +) : DeletePrescriptionsBridge { + + private val prescription by lazy { + generatePrescriptionDetailsUseCase(taskId).stateIn(scope, SharingStarted.Lazily, null) + } + + fun redeemScannedTask(taskId: String, redeem: Boolean) { + scope.launch { + prescriptionUseCase.redeemScannedTask(taskId, redeem) + } + } + + fun updateScannedTaskName(taskId: String, name: String) { + scope.launch { + updateScannedTaskNameUseCase.invoke(taskId, name) + } + } + + override suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result = + prescriptionUseCase.deletePrescription(profileId = profileId, taskId = taskId) + + val prescriptionState + @Composable + get() = prescription.collectAsStateWithLifecycle() +} + +@Composable +fun rememberPrescriptionDetailsController(taskId: String): PrescriptionDetailsController { + val generatePrescriptionDetailsUseCase by rememberInstance() + val prescriptionUseCase by rememberInstance() + val updateScannedTaskNameUseCase by rememberInstance() + val scope = rememberCoroutineScope() + return remember { + PrescriptionDetailsController( + taskId = taskId, + generatePrescriptionDetailsUseCase = generatePrescriptionDetailsUseCase, + prescriptionUseCase = prescriptionUseCase, + updateScannedTaskNameUseCase = updateScannedTaskNameUseCase, + scope = scope + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsScaffold.kt new file mode 100644 index 00000000..386f29ec --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/PrescriptionDetailsScaffold.kt @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.MutatorMutex +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ScaffoldState +import androidx.compose.material.SnackbarHost +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.core.LocalAuthenticator +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.ui.PrescriptionServiceErrorState +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode +import kotlinx.coroutines.launch + +@Composable +fun PrescriptionDetailsScaffold( + scaffoldState: ScaffoldState, + listState: LazyListState, + onBack: () -> Unit, + prescriptionDetailsController: PrescriptionDetailsController, + prescription: PrescriptionData.Prescription?, + navController: NavHostController, + onClickMedication: (PrescriptionData.Medication) -> Unit, + onChangeSheetContent: (PrescriptionDetailBottomSheetContent?) -> Unit +) { + AnimatedElevationScaffold( + scaffoldState = scaffoldState, + listState = listState, + onBack = onBack, + topBarTitle = stringResource(R.string.prescription_details), + navigationMode = NavigationBarMode.Close, + snackbarHost = { SnackbarHost(it, modifier = Modifier.navigationBarsPadding()) }, + actions = { + val context = LocalContext.current + val authenticator = LocalAuthenticator.current + val deletePrescriptionsHandle = remember { + DeletePrescriptions( + prescriptionDetailsController = prescriptionDetailsController, + authenticator = authenticator + ) + } + + prescription?.let { + DeleteAction(it) { + // TODO: This needs to be done in the PrescriptionDetailsScreen, + // please do stateHoisting so that it is not hidden inside, becase this also has a onBack. + // It should be something like maybeOnBack with the deleteState sent with it and then in the + // screen we decide what to do + val deleteState = deletePrescriptionsHandle.deletePrescription( + profileId = prescription.profileId, + taskId = prescription.taskId + ) + + when (deleteState) { + is PrescriptionServiceErrorState -> { + deleteErrorMessage(context, deleteState)?.let { + scaffoldState.snackbarHostState.showSnackbar(it) + } + } + + is DeletePrescriptions.State.Deleted -> onBack() + } + } + } + } + ) { + when (prescription) { + is PrescriptionData.Synced -> + SyncedPrescriptionOverview( + navController = navController, + listState = listState, + prescription = prescription, + onSelectMedication = onClickMedication, + onShowInfo = { + onChangeSheetContent(it) + } + ) + + is PrescriptionData.Scanned -> + ScannedPrescriptionOverview( + navController = navController, + listState = listState, + prescription = prescription, + onSwitchRedeemed = { + prescriptionDetailsController.redeemScannedTask( + taskId = prescription.taskId, + redeem = it + ) + }, + onShowInfo = { + onChangeSheetContent(it) + }, + onChangePrescriptionName = { newName -> + prescriptionDetailsController.updateScannedTaskName(prescription.taskId, newName) + } + ) + + else -> { + // do nothing + } + } + } +} + +@Composable +private fun DeleteAction( + prescription: PrescriptionData.Prescription, + onClickDelete: suspend () -> Unit +) { + var showDeletePrescriptionDialog by remember { mutableStateOf(false) } + var deletionInProgress by remember { mutableStateOf(false) } + + val coroutineScope = rememberCoroutineScope() + val mutex = MutatorMutex() + + var dropdownExpanded by remember { mutableStateOf(false) } + + val isDeletable by remember { + derivedStateOf { + (prescription as? PrescriptionData.Synced)?.isDeletable ?: true + } + } + + IconButton( + onClick = { dropdownExpanded = true }, + modifier = Modifier.testTag(TestTag.Prescriptions.Details.MoreButton) + ) { + Icon(Icons.Rounded.MoreVert, null, tint = AppTheme.colors.neutral600) + } + DropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false }, + offset = DpOffset(24.dp, 0.dp) + ) { + DropdownMenuItem( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.DeleteButton), + enabled = isDeletable, + onClick = { + dropdownExpanded = false + showDeletePrescriptionDialog = true + } + ) { + Text( + text = stringResource(R.string.pres_detail_dropdown_delete), + color = if (isDeletable) { + AppTheme.colors.red600 + } else { + AppTheme.colors.neutral400 + } + ) + } + } + + if (showDeletePrescriptionDialog) { + val info = stringResource(R.string.pres_detail_delete_msg) + val cancelText = stringResource(R.string.pres_detail_delete_no) + val actionText = stringResource(R.string.pres_detail_delete_yes) + + CommonAlertDialog( + header = null, + info = info, + cancelText = cancelText, + actionText = actionText, + enabled = !deletionInProgress, + onCancel = { + showDeletePrescriptionDialog = false + }, + onClickAction = { + coroutineScope.launch { + mutex.mutate { + try { + deletionInProgress = true + onClickDelete() + } finally { + showDeletePrescriptionDialog = false + deletionInProgress = false + } + } + } + } + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt new file mode 100644 index 00000000..bd4642d1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/ScannedPrescriptionOverview.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsNavigationScreens +import de.gematik.ti.erp.app.prescription.ui.SentStatusChip +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.EditableHeaderTextField +import de.gematik.ti.erp.app.utils.compose.HealthPortalLink +import de.gematik.ti.erp.app.utils.compose.Label +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonSmall +import de.gematik.ti.erp.app.utils.compose.SpacerShortMedium +import de.gematik.ti.erp.app.utils.compose.SpacerXLarge +import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge +import de.gematik.ti.erp.app.utils.compose.dateWithIntroductionString + +@Composable +fun ScannedPrescriptionOverview( + navController: NavController, + listState: LazyListState, + prescription: PrescriptionData.Scanned, + onSwitchRedeemed: (redeemed: Boolean) -> Unit, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit, + onChangePrescriptionName: (String) -> Unit +) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize(), + contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + ) { + item { + Column( + Modifier + .fillMaxWidth() + .padding(PaddingDefaults.Medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val titlePrepend = stringResource(R.string.pres_details_scanned_medication) + + val prescriptionName = prescription.name ?: "$titlePrepend ${prescription.index}" + + EditableHeaderTextField( + text = prescriptionName, + onSaveText = { onChangePrescriptionName(it) } + ) + + SpacerShortMedium() + Row( + modifier = Modifier.clickable { + onShowInfo(PrescriptionDetailBottomSheetContent.Scanned()) + }, + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + val date = dateWithIntroductionString(R.string.prs_low_detail_scanned_on, prescription.scannedOn) + Text(date, style = AppTheme.typography.body2l) + Icon(Icons.Rounded.KeyboardArrowRight, null, tint = AppTheme.colors.primary600) + } + if (prescription.task.communications.isNotEmpty()) { + SpacerShortMedium() + SentStatusChip() + } + } + } + + item { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = PaddingDefaults.Medium) + ) { + SpacerXLarge() + RedeemedButton( + modifier = Modifier.align(Alignment.CenterHorizontally), + redeemed = prescription.isRedeemed, + onSwitchRedeemed = onSwitchRedeemed + ) + SpacerXXLarge() + } + } + + item { + Label( + text = stringResource(R.string.pres_detail_technical_information), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.TechnicalInformation.path()) + } + ) + } + + item { + HealthPortalLink(Modifier.padding(horizontal = PaddingDefaults.Medium, vertical = PaddingDefaults.XXLarge)) + } + } +} + +@Composable +private fun RedeemedButton( + modifier: Modifier, + redeemed: Boolean, + onSwitchRedeemed: (redeemed: Boolean) -> Unit +) { + val buttonText = if (redeemed) { + stringResource(R.string.scanned_prescription_details_mark_as_unredeemed) + } else { + stringResource(R.string.scanned_prescription_details_mark_as_redeemed) + } + + PrimaryButtonSmall( + onClick = { + onSwitchRedeemed(!redeemed) + }, + modifier = modifier + ) { + Text(buttonText) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt index daa31f30..cfb1637e 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SharePrescriptionController.kt @@ -26,13 +26,13 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.core.LocalIntentHandler +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.userauthentication.ui.AuthenticationModeAndMethod import de.gematik.ti.erp.app.utils.compose.shortToast import io.github.aakira.napier.Napier @@ -48,6 +48,7 @@ import java.net.URLEncoder private const val ShareBaseUri = "https://das-e-rezept-fuer-deutschland.de/prescription/#" +// TODO: Check if needed, not used anywhere @Stable class SharePrescriptionController( prescriptionUseCase: LazyDelegate, @@ -98,6 +99,8 @@ class SharePrescriptionController( tasks = listOf( ScannedTaskData.ScannedTask( profileId = profileId, + index = 0, + name = null, taskId = taskId, accessCode = accessCode, scannedOn = Clock.System.now(), @@ -152,9 +155,9 @@ fun rememberSharePrescriptionController( @Composable fun SharePrescriptionHandler( + activeProfile: ProfilesUseCaseData.Profile, authenticationModeAndMethod: Flow ) { - val activeProfile = LocalProfileHandler.current.activeProfile val controller = rememberSharePrescriptionController(activeProfile.id) val intentHandler = LocalIntentHandler.current val context = LocalContext.current diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt index cd5ae7d2..615415a5 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedMedicationDetailScreen.kt @@ -33,15 +33,15 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.SnackbarHost import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.medicationCategory import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData @@ -49,23 +49,26 @@ import de.gematik.ti.erp.app.prescription.repository.codeToFormMapping import de.gematik.ti.erp.app.prescription.repository.normSizeMapping import de.gematik.ti.erp.app.substitutionAllowed import de.gematik.ti.erp.app.supplyForm +import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.annotatedStringResource -import de.gematik.ti.erp.app.utils.dateTimeMediumText -import de.gematik.ti.erp.app.utils.temporalText +import de.gematik.ti.erp.app.utils.extensions.dateTimeMediumText +import de.gematik.ti.erp.app.utils.extensions.temporalText import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone @Composable fun SyncedMedicationDetailScreen( - prescription: PrescriptionData.Synced, medication: PrescriptionData.Medication, onClickIngredient: (SyncedTaskData.Ingredient) -> Unit, - onBack: () -> Unit + onBack: () -> Unit, + prescriptionDetailsController: PrescriptionDetailsController ) { + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced val scaffoldState = rememberScaffoldState() val listState = rememberLazyListState() @@ -102,7 +105,7 @@ fun SyncedMedicationDetailScreen( null -> {} } - prescriptionInformation(prescription.authoredOn) + syncedPrescription?.authoredOn?.let { prescriptionInformation(it) } when (medication) { is PrescriptionData.Medication.Dispense -> diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt new file mode 100644 index 00000000..da0307e1 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/SyncedPrescriptionOverview.kt @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.detail.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionDetailsNavigationScreens +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.utils.compose.HealthPortalLink +import de.gematik.ti.erp.app.utils.compose.Label +import de.gematik.ti.erp.app.utils.compose.PrimaryButtonTiny +import de.gematik.ti.erp.app.utils.compose.SpacerMedium +import de.gematik.ti.erp.app.utils.compose.handleIntent +import de.gematik.ti.erp.app.utils.compose.provideEmailIntent + +@Composable +fun SyncedPrescriptionOverview( + navController: NavController, + listState: LazyListState, + prescription: PrescriptionData.Synced, + onSelectMedication: (PrescriptionData.Medication) -> Unit, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +) { + val noValueText = stringResource(R.string.pres_details_no_value) + + Column { + val colPadding = if (prescription.isIncomplete) { + PaddingValues() + } else { + WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() + } + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag(TestTag.Prescriptions.Details.Content), + contentPadding = colPadding + ) { + item { + SyncedHeader( + prescription = prescription, + onShowInfo = onShowInfo + ) + } + + item { + val text = additionalFeeText(prescription.medicationRequest.additionalFee) ?: noValueText + + Label( + text = text, + label = stringResource(R.string.pres_details_additional_fee), + onClick = onClickAdditionalFee( + prescription.medicationRequest.additionalFee, + onShowInfo + ) + ) + } + + prescription.medicationRequest.emergencyFee?.let { emergencyFee -> + item { + Label( + text = stringResource( + if (emergencyFee) R.string.pres_detail_noctu_no else R.string.pres_detail_noctu_yes + ), + label = stringResource(R.string.pres_details_emergency_fee), + onClick = onClickEmergencyFee(emergencyFee, onShowInfo) + ) + } + } + + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.MedicationButton), + text = prescription.name ?: noValueText, + label = stringResource(R.string.pres_details_medication), + onClick = onClickMedication(prescription, onSelectMedication, navController) + ) + } + + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PatientButton), + text = prescription.patient.name ?: noValueText, + label = stringResource(R.string.pres_detail_patient_header), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.Patient.path()) + } + ) + } + + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.PrescriberButton), + text = prescription.practitioner.name ?: noValueText, + label = stringResource(R.string.pres_detail_practitioner_header), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.Prescriber.path()) + } + ) + } + + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.OrganizationButton), + text = prescription.organization.name ?: noValueText, + label = stringResource(R.string.pres_detail_organization_header), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.Organization.path()) + } + ) + } + + item { + Label( + text = stringResource(R.string.pres_detail_accident_header), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.Accident.path()) + } + ) + } + + item { + Label( + modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformationButton), + text = stringResource(R.string.pres_detail_technical_information), + onClick = { + navController.navigate(PrescriptionDetailsNavigationScreens.TechnicalInformation.path()) + } + ) + } + + item { + HealthPortalLink( + Modifier.padding( + horizontal = PaddingDefaults.Medium, + vertical = PaddingDefaults.XXLarge + ) + ) + } + } + + if (prescription.isIncomplete) { + FailureBanner( + Modifier + .fillMaxWidth() + .navigationBarsPadding(), + prescription + ) + } + } +} + +@Composable +private fun additionalFeeText(additionalFee: SyncedTaskData.AdditionalFee): String? = when (additionalFee) { + SyncedTaskData.AdditionalFee.Exempt -> + stringResource(R.string.pres_detail_no) + SyncedTaskData.AdditionalFee.NotExempt -> + stringResource(R.string.pres_detail_yes) + else -> null +} + +@Composable +private fun FailureBanner( + modifier: Modifier, + prescription: PrescriptionData.Synced +) { + val mailAddress = stringResource(R.string.settings_contact_mail_address) + val subject = stringResource(R.string.settings_feedback_mail_subject) + + val context = LocalContext.current + Row( + modifier + .fillMaxWidth() + .background(AppTheme.colors.neutral050) + .padding(PaddingDefaults.Medium), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(R.string.prescription_failure_info), + style = AppTheme.typography.body2, + modifier = Modifier.weight(1f) + ) + SpacerMedium() + PrimaryButtonTiny( + onClick = { + val body = """ + PVS ID: ${prescription.task.pvsIdentifier} + + ${prescription.failureToReport} + """.trimIndent() + + context.handleIntent( + provideEmailIntent( + address = mailAddress, + body = body, + subject = subject + ) + ) + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = AppTheme.colors.red600, + contentColor = AppTheme.colors.neutral000 + ) + ) { + Text(stringResource(R.string.report_prescription_failure)) + } + } +} + +@Composable +private fun onClickAdditionalFee( + additionalFee: SyncedTaskData.AdditionalFee, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +): () -> Unit = { + when (additionalFee) { + SyncedTaskData.AdditionalFee.NotExempt -> { + onShowInfo( + PrescriptionDetailBottomSheetContent.AdditionalFeeNotExempt() + ) + } + SyncedTaskData.AdditionalFee.Exempt -> { + onShowInfo( + PrescriptionDetailBottomSheetContent.AdditionalFeeExempt() + ) + } + else -> {} + } +} + +@Composable +private fun onClickMedication( + prescription: PrescriptionData.Synced, + onSelectMedication: (PrescriptionData.Medication) -> Unit, + navController: NavController +): () -> Unit = { + if (!prescription.isDispensed) { + onSelectMedication(PrescriptionData.Medication.Request(prescription.medicationRequest)) + } else { + navController.navigate(PrescriptionDetailsNavigationScreens.MedicationOverview.path()) + } +} + +@Composable +private fun onClickEmergencyFee( + emergencyFee: Boolean, + onShowInfo: (PrescriptionDetailBottomSheetContent) -> Unit +): () -> Unit = { + if (emergencyFee) { + onShowInfo( + PrescriptionDetailBottomSheetContent.EmergencyFeeNotExempt() + ) + } else { + onShowInfo( + PrescriptionDetailBottomSheetContent.EmergencyFee() + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt index dd661485..904a48f4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/TechnicalInformation.kt @@ -27,11 +27,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.Label @@ -40,9 +41,13 @@ import de.gematik.ti.erp.app.utils.compose.SpacerMedium @Composable fun TechnicalInformation( - prescription: PrescriptionData.Prescription, + taskId: String, + prescriptionDetailsController: PrescriptionDetailsController, onBack: () -> Unit ) { + val prescription by prescriptionDetailsController.prescriptionState + val syncedPrescription = prescription as? PrescriptionData.Synced + val listState = rememberLazyListState() AnimatedElevationScaffold( modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.Screen), @@ -61,7 +66,7 @@ fun TechnicalInformation( item { SpacerMedium() } - prescription.accessCode?.let { + syncedPrescription?.accessCode?.let { item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.AccessCode), @@ -74,7 +79,7 @@ fun TechnicalInformation( item { Label( modifier = Modifier.testTag(TestTag.Prescriptions.Details.TechnicalInformation.TaskId), - text = prescription.taskId, + text = syncedPrescription?.taskId, label = stringResource(R.string.task_id) ) SpacerMedium() diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/Navigation.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt index 715f66d8..ff0a9193 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/detail/ui/model/PrescriptionData.kt @@ -44,6 +44,8 @@ object PrescriptionData { override val redeemedOn: Instant? = task.redeemedOn override val accessCode: String = task.accessCode val scannedOn: Instant = task.scannedOn + val index: Int = task.index + val name: String? = task.name val isRedeemed = redeemedOn != null } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt index d8e6b02c..0ec88b76 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/mapper/PrescriptionMapper.kt @@ -25,6 +25,8 @@ import de.gematik.ti.erp.app.prescription.usecase.model.Prescription internal fun ScannedTask.toPrescription() = Prescription.ScannedPrescription( taskId = taskId, scannedOn = scannedOn, + name = name, + index = index, redeemedOn = redeemedOn, communications = communications ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt index 99212d21..c6708c7f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/repository/KBVCodeMapping.kt @@ -18,7 +18,7 @@ package de.gematik.ti.erp.app.prescription.repository -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R val statusMapping = mapOf( "1" to R.string.kbv_member_status_1, diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt index 87648648..018c0b68 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ArchiveScreen.kt @@ -18,6 +18,8 @@ package de.gematik.ti.erp.app.prescription.ui +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn @@ -31,9 +33,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.navigation.NavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription import de.gematik.ti.erp.app.theme.AppTheme @@ -45,6 +47,7 @@ import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime import java.time.format.DateTimeFormatter +@RequiresApi(Build.VERSION_CODES.O) @Composable fun ArchiveScreen( prescriptionsController: PrescriptionsController, @@ -103,7 +106,6 @@ fun ArchiveScreen( LowDetailMedication( modifier = CardPaddingModifier, prescription, - 0, onClick = { navController.navigate( MainNavigationScreens.PrescriptionDetail.path( diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreenTemplate.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt index 7fac5f75..4dd2e343 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/EmptyScreens.kt @@ -44,8 +44,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerSmall diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt index b21055e9..412144d1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MainScreenAvatar.kt @@ -45,18 +45,19 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.profiles.ui.ChooseAvatar -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler import de.gematik.ti.erp.app.profiles.ui.profileColor +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.TertiaryButton @Composable -fun SmallMainScreenAvatar(onClickAvatar: () -> Unit) { - val profileHandler = LocalProfileHandler.current - val profile = profileHandler.activeProfile +fun SmallMainScreenAvatar( + profile: ProfilesUseCaseData.Profile, + onClickAvatar: () -> Unit +) { val ssoTokenScope = profile.ssoTokenScope val currentSelectedColors = profileColor(profileColorNames = profile.color) @@ -77,10 +78,10 @@ fun SmallMainScreenAvatar(onClickAvatar: () -> Unit) { contentAlignment = Alignment.Center ) { ChooseAvatar( - iconModifier = Modifier.size(16.dp), + modifier = Modifier.size(16.dp), emptyIcon = Icons.Rounded.AddAPhoto, profile = profile, - figure = profile.avatarFigure + figure = profile.avatar ) } } @@ -115,6 +116,7 @@ fun SmallMainScreenAvatar(onClickAvatar: () -> Unit) { modifier = Modifier.size(12.dp).align(Alignment.Center) ) } + else -> { Icon( Icons.Rounded.Close, @@ -130,11 +132,10 @@ fun SmallMainScreenAvatar(onClickAvatar: () -> Unit) { } @Composable -fun MainScreenAvatar(onClickAvatar: () -> Unit) { - val profileHandler = LocalProfileHandler.current - val profile = profileHandler.activeProfile - val ssoTokenScope = profile.ssoTokenScope - +fun MainScreenAvatar( + profile: ProfilesUseCaseData.Profile, + onClickAvatar: () -> Unit +) { val currentSelectedColors = profileColor(profileColorNames = profile.color) Box( @@ -153,10 +154,10 @@ fun MainScreenAvatar(onClickAvatar: () -> Unit) { contentAlignment = Alignment.Center ) { ChooseAvatar( - iconModifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp), emptyIcon = Icons.Rounded.AddAPhoto, profile = profile, - figure = profile.avatarFigure + figure = profile.avatar ) } } @@ -176,13 +177,14 @@ fun MainScreenAvatar(onClickAvatar: () -> Unit) { ) { when { - ssoTokenScope?.token?.isValid() == true -> { + profile.ssoTokenScope?.token?.isValid() == true -> { Image( painterResource(R.drawable.main_screen_erx_icon_large), null, modifier = Modifier.align(Alignment.Center) ) } + else -> { Image( painterResource(R.drawable.main_screen_erx_icon_gray_large), @@ -197,22 +199,33 @@ fun MainScreenAvatar(onClickAvatar: () -> Unit) { } @Composable -fun ProfileConnectionSection(onClickAvatar: () -> Unit, onClickRefresh: () -> Unit) { +fun ProfileConnectionSection( + activeProfile: ProfilesUseCaseData.Profile, + onClickAvatar: () -> Unit, + onClickRefresh: () -> Unit +) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = PaddingDefaults.Medium), horizontalArrangement = Arrangement.SpaceBetween ) { - SmallMainScreenAvatar(onClickAvatar) - ConnectionHelper(onClickRefresh) + SmallMainScreenAvatar( + profile = activeProfile, + onClickAvatar = onClickAvatar + ) + ConnectionHelper( + profile = activeProfile, + onClickRefresh = onClickRefresh + ) } } @Composable -fun ConnectionHelper(onClickRefresh: () -> Unit) { - val profileHandler = LocalProfileHandler.current - val profile = profileHandler.activeProfile +fun ConnectionHelper( + profile: ProfilesUseCaseData.Profile, + onClickRefresh: () -> Unit +) { val ssoTokenScope = profile.ssoTokenScope if (ssoTokenScope?.token == null) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt index 6de91089..cde7aae4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitInformationScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.navigation.NavController -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt index 5c525489..8fc09478 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/MlKitIntroScreen.kt @@ -40,9 +40,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.settings.ui.SettingsController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt index 13865901..06613629 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreenComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionScreen.kt @@ -65,12 +65,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em -import androidx.navigation.NavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens -import de.gematik.ti.erp.app.mainscreen.ui.MainScreenController +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.presentation.MainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData.scannedPrescriptionIndex import de.gematik.ti.erp.app.prescription.model.SyncedTaskData @@ -80,8 +78,9 @@ import de.gematik.ti.erp.app.prescription.usecase.model.Prescription import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.ScannedPrescription import de.gematik.ti.erp.app.prescription.usecase.model.Prescription.SyncedPrescription import de.gematik.ti.erp.app.prescriptionId -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler -import de.gematik.ti.erp.app.profiles.ui.ProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.ProfileConnectionState +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.connectionState import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog @@ -110,24 +109,46 @@ const val ONE_DAY_LEFT = 1L const val TWO_DAYS_LEFT = 2L @Composable -fun PrescriptionScreen( - navController: NavController, - prescriptionsController: PrescriptionsController, +fun PrescriptionsScreen( + controller: PrescriptionsController, + mainScreenController: MainScreenController, // TODO: Hoist it out + activeProfile: ProfilesUseCaseData.Profile, + onShowCardWall: () -> Unit, + onElevateTopBar: (Boolean) -> Unit, + onClickPrescription: (String) -> Unit, + onClickAvatar: () -> Unit, + onClickArchive: () -> Unit +) { + val activePrescriptions by controller.activePrescriptionsState + val archivedPrescriptions by controller.archivedPrescriptionsState + + PrescriptionScreen( + activeProfile = activeProfile, + activePrescriptions = activePrescriptions, + isArchiveEmpty = archivedPrescriptions.isEmpty(), + mainScreenController = mainScreenController, + onElevateTopBar = onElevateTopBar, + onClickPrescription = onClickPrescription, + onClickAvatar = onClickAvatar, + onClickArchive = onClickArchive, + onShowCardWall = onShowCardWall + ) +} + +@Composable +private fun PrescriptionScreen( + activeProfile: ProfilesUseCaseData.Profile, + activePrescriptions: List, + isArchiveEmpty: Boolean, mainScreenController: MainScreenController, + onElevateTopBar: (Boolean) -> Unit, + onClickPrescription: (String) -> Unit, + onShowCardWall: () -> Unit, onClickAvatar: () -> Unit, - onClickArchive: () -> Unit, - onElevateTopBar: (Boolean) -> Unit + onClickArchive: () -> Unit ) { - val profileHandler = LocalProfileHandler.current - val profileId = profileHandler.activeProfile.id var showUserNotAuthenticatedDialog by remember { mutableStateOf(false) } - val onShowCardWall = { - navController.navigate( - MainNavigationScreens.CardWall.path(profileHandler.activeProfile.id) - ) - } - if (showUserNotAuthenticatedDialog) { UserNotAuthenticatedDialog( onCancel = { showUserNotAuthenticatedDialog = false }, @@ -136,20 +157,20 @@ fun PrescriptionScreen( } RefreshScaffold( - profileId = profileId, + profileId = activeProfile.id, onUserNotAuthenticated = { showUserNotAuthenticatedDialog = true }, mainScreenController = mainScreenController, onShowCardWall = onShowCardWall ) { onRefresh -> Prescriptions( - prescriptionsController = prescriptionsController, - onClickRefresh = { - onRefresh(true, MutatePriority.UserInput) - }, + activeProfile = activeProfile, + activePrescriptions = activePrescriptions, + isArchiveEmpty = isArchiveEmpty, + onClickRefresh = { onRefresh(true, MutatePriority.UserInput) }, onClickAvatar = onClickAvatar, - navController = navController, onElevateTopBar = onElevateTopBar, - onClickArchive = onClickArchive + onClickArchive = onClickArchive, + onClickPrescription = onClickPrescription ) } } @@ -182,23 +203,23 @@ val CardPaddingModifier = Modifier @Composable private fun Prescriptions( - prescriptionsController: PrescriptionsController, - navController: NavController, + activeProfile: ProfilesUseCaseData.Profile, + activePrescriptions: List, + isArchiveEmpty: Boolean, + onClickPrescription: (String) -> Unit, + onElevateTopBar: (Boolean) -> Unit, onClickRefresh: () -> Unit, onClickAvatar: () -> Unit, - onClickArchive: () -> Unit, - onElevateTopBar: (Boolean) -> Unit + onClickArchive: () -> Unit ) { - val activePrescriptions by prescriptionsController.activePrescriptionsState - val archivedPrescriptions by prescriptionsController.archivedPrescriptionsState - PrescriptionsContent( - onClickRefresh = onClickRefresh, - onClickAvatar = onClickAvatar, + activeProfile = activeProfile, activePrescriptions = activePrescriptions, - isArchiveEmpty = archivedPrescriptions.isEmpty(), - navController = navController, + isArchiveEmpty = isArchiveEmpty, onElevateTopBar = onElevateTopBar, + onClickPrescription = onClickPrescription, + onClickRefresh = onClickRefresh, + onClickAvatar = onClickAvatar, onClickArchive = onClickArchive ) } @@ -207,16 +228,16 @@ private val FabPadding = 68.dp @Composable private fun PrescriptionsContent( - onClickRefresh: () -> Unit, - onClickAvatar: () -> Unit, - onClickArchive: () -> Unit, + activeProfile: ProfilesUseCaseData.Profile, activePrescriptions: List, isArchiveEmpty: Boolean, - navController: NavController, - onElevateTopBar: (Boolean) -> Unit + onElevateTopBar: (Boolean) -> Unit, + onClickPrescription: (String) -> Unit, + onClickRefresh: () -> Unit, + onClickAvatar: () -> Unit, + onClickArchive: () -> Unit ) { val listState = rememberLazyListState() - val profileHandler = LocalProfileHandler.current LaunchedEffect(Unit) { snapshotFlow { @@ -238,16 +259,23 @@ private fun PrescriptionsContent( if (activePrescriptions.isNotEmpty()) { item { SpacerXXLarge() - ProfileConnectionSection(onClickAvatar, onClickRefresh) + ProfileConnectionSection( + activeProfile = activeProfile, + onClickAvatar = onClickAvatar, + onClickRefresh = onClickRefresh + ) SpacerMedium() } - prescriptionContent( activePrescriptions = activePrescriptions, - navController = navController + onClickPrescription = onClickPrescription ) } else { - emptyContent(profileHandler, onClickRefresh, onClickAvatar) + emptyContent( + activeProfile = activeProfile, + onClickConnect = onClickRefresh, + onClickAvatar = onClickAvatar + ) } if (!isArchiveEmpty) { item { @@ -265,17 +293,18 @@ private fun PrescriptionsContent( } fun LazyListScope.emptyContent( - profileHandler: ProfileHandler, + activeProfile: ProfilesUseCaseData.Profile, onClickConnect: () -> Unit, onClickAvatar: () -> Unit ) { item { Spacer(modifier = Modifier.size(80.dp)) - MainScreenAvatar(onClickAvatar) + MainScreenAvatar( + profile = activeProfile, + onClickAvatar = onClickAvatar + ) } - if (profileHandler.connectionState(profileHandler.activeProfile) != - ProfileHandler.ProfileConnectionState.LoggedIn - ) { + if (activeProfile.connectionState() != ProfileConnectionState.LoggedIn) { item { SpacerMedium() TertiaryButton(onClickConnect, modifier = Modifier.testTag(TestTag.Main.LoginButton)) { @@ -322,8 +351,8 @@ fun LazyListScope.emptyContent( } private fun LazyListScope.prescriptionContent( - navController: NavController, - activePrescriptions: List + activePrescriptions: List, + onClickPrescription: (String) -> Unit ) { val prescriptionsIndices = processPrescriptionsDayIndices(activePrescriptions) @@ -335,11 +364,7 @@ private fun LazyListScope.prescriptionContent( prescription, modifier = CardPaddingModifier, onClick = { - navController.navigate( - MainNavigationScreens.PrescriptionDetail.path( - taskId = prescription.taskId - ) - ) + onClickPrescription(prescription.taskId) } ) @@ -347,15 +372,10 @@ private fun LazyListScope.prescriptionContent( LowDetailMedication( modifier = CardPaddingModifier, prescription, - prescriptionsIndices.getOrDefault(prescription.taskId, 1), onClick = { scannedPrescriptionIndex = prescriptionsIndices.getOrDefault(prescription.taskId, 1) - navController.navigate( - MainNavigationScreens.PrescriptionDetail.path( - taskId = prescription.taskId - ) - ) + onClickPrescription(prescription.taskId) } ) } @@ -702,7 +722,6 @@ fun FullDetailMedication( fun LowDetailMedication( modifier: Modifier = Modifier, prescription: ScannedPrescription, - index: Int, onClick: () -> Unit ) { val dateFormatter = remember { DateTimeFormatter.ofPattern("dd.MM.yyyy") } @@ -736,12 +755,12 @@ fun LowDetailMedication( modifier = Modifier .weight(1f) ) { + val titlePrepend = stringResource(R.string.pres_details_scanned_medication) + + val name = prescription.name ?: "$titlePrepend ${prescription.index}" + Text( - if (index > 0) { - stringResource(R.string.prs_low_detail_medication) + " $index" - } else { - stringResource(R.string.prs_low_detail_medication) - }, + name, style = AppTheme.typography.subtitle1 ) SpacerTiny() diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionServiceState.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt similarity index 61% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt index affe58a6..97314d45 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionsController.kt @@ -15,51 +15,65 @@ * limitations under the Licence. * */ +@file:Suppress("UnusedPrivateMember") 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.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.core.complexAutoSaver import de.gematik.ti.erp.app.prescription.usecase.GetActivePrescriptionsUseCase import de.gematik.ti.erp.app.prescription.usecase.GetArchivedPrescriptionsUseCase -import de.gematik.ti.erp.app.prescription.usecase.model.Prescription +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController.Companion.DEFAULT_EMPTY_PROFILE import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler -import kotlinx.coroutines.flow.Flow +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly +import kotlinx.coroutines.flow.stateIn import org.kodein.di.compose.rememberInstance @Stable class PrescriptionsController( private val activePrescriptionsUseCase: GetActivePrescriptionsUseCase, private val archivedPrescriptionsUseCase: GetArchivedPrescriptionsUseCase, - private val profileId: ProfileIdentifier + private val profileId: ProfileIdentifier, + scope: CoroutineScope ) { - private val activePrescriptions: Flow> - get() = activePrescriptionsUseCase(profileId) - private val archivedPrescriptions: Flow> - get() = archivedPrescriptionsUseCase(profileId) + private val activePrescriptions by lazy { + activePrescriptionsUseCase(profileId).stateIn(scope, Eagerly, emptyList()) + } + + private val archivedPrescriptions by lazy { + archivedPrescriptionsUseCase(profileId).stateIn(scope, Eagerly, emptyList()) + } + val activePrescriptionsState @Composable - get() = activePrescriptions.collectAsState(emptyList()) + get() = activePrescriptions.collectAsStateWithLifecycle(emptyList()) val archivedPrescriptionsState @Composable - get() = archivedPrescriptions.collectAsState(emptyList()) + get() = archivedPrescriptions.collectAsStateWithLifecycle(emptyList()) } @Composable fun rememberPrescriptionsController(): PrescriptionsController { val activePrescriptionsUseCase by rememberInstance() val archivedPrescriptionsUseCase by rememberInstance() - val activeProfile = LocalProfileHandler.current.activeProfile + val getActiveProfileUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + val activeProfile by getActiveProfileUseCase().collectAsStateWithLifecycle(DEFAULT_EMPTY_PROFILE) return rememberSaveable(activeProfile.id, saver = complexAutoSaver()) { PrescriptionsController( + profileId = activeProfile.id, activePrescriptionsUseCase = activePrescriptionsUseCase, archivedPrescriptionsUseCase = archivedPrescriptionsUseCase, - profileId = activeProfile.id + scope = scope ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt index 8a6f1e88..0a60bad7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/RefreshPrescriptionsController.kt @@ -20,17 +20,17 @@ 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.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.Authenticator import de.gematik.ti.erp.app.cardwall.mini.ui.NoneEnrolledException -import de.gematik.ti.erp.app.cardwall.mini.ui.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException import de.gematik.ti.erp.app.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.MainScreenController +import de.gematik.ti.erp.app.mainscreen.presentation.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 @@ -57,7 +57,7 @@ class RefreshPrescriptionsController( val isRefreshing @Composable - get() = refreshPrescriptionUseCase.refreshInProgress.collectAsState() + get() = refreshPrescriptionUseCase.refreshInProgress.collectAsStateWithLifecycle() suspend fun refresh( profileId: ProfileIdentifier, @@ -123,7 +123,7 @@ fun Flow.retryWithAuthenticator( when { !isUserAction -> throw CancellationException("Authentication cancelled due `isUserAction = false`") - (throwable.cause as? RefreshFlowException)?.userActionRequired == true -> { + (throwable.cause as? RefreshFlowException)?.isUserAction == true -> { authenticate .first() .let { diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt index 9d0dc13b..dbab065c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanPrescriptionController.kt @@ -19,16 +19,17 @@ package de.gematik.ti.erp.app.prescription.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.prescription.ui.model.ScanData 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.GetActiveProfileUseCase import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -73,7 +74,8 @@ private data class ScanWorkflow( @OptIn(ExperimentalCoroutinesApi::class) class ScanPrescriptionController( private val prescriptionUseCase: PrescriptionUseCase, - private val profilesUseCase: ProfilesUseCase, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + // private val profilesUseCase: ProfilesUseCase, val scanner: TwoDCodeScanner, val processor: TwoDCodeProcessor, private val validator: TwoDCodeValidator, @@ -109,7 +111,7 @@ class ScanPrescriptionController( val state @Composable - get() = stateFlow.collectAsState(ScanData.defaultState) + get() = stateFlow.collectAsStateWithLifecycle(ScanData.defaultState) private val scanOverlayFlow = flow { val batchFlow = scanner.batch.mapNotNull { batch -> @@ -189,11 +191,11 @@ class ScanPrescriptionController( ) ) } - }.flowOn(dispatchers.Default) + }.flowOn(dispatchers.default) val overlayState @Composable - get() = scanOverlayFlow.collectAsState(ScanData.defaultOverlayState) + get() = scanOverlayFlow.collectAsStateWithLifecycle(ScanData.defaultOverlayState) private fun validateScannedCode(scannedCode: ScannedCode): ValidScannedCode? = validator.validate(scannedCode) @@ -215,23 +217,32 @@ class ScanPrescriptionController( } suspend fun saveToDatabase() { - prescriptionUseCase.saveScannedCodes( - profilesUseCase.activeProfile.first().id, - scannedCodes.value - ) + getActiveProfileUseCase().collectLatest { profile -> + prescriptionUseCase.saveScannedCodes( + profile.id, + scannedCodes.value + ) + } } } @Composable fun rememberScanPrescriptionController(): ScanPrescriptionController { val prescriptionUseCase by rememberInstance() - val profilesUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() val scanner by rememberInstance() val processor by rememberInstance() val validator by rememberInstance() val dispatchers by rememberInstance() return remember { - ScanPrescriptionController(prescriptionUseCase, profilesUseCase, scanner, processor, validator, dispatchers) + ScanPrescriptionController( + prescriptionUseCase = prescriptionUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + scanner = scanner, + processor = processor, + validator = validator, + dispatchers = dispatchers + ) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt index da173ee2..551282cd 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/ScanScreenComponent.kt @@ -63,8 +63,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults @@ -120,22 +122,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.navigation.NavController -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.systemBarsPadding -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.trackScannerPopUps import de.gematik.ti.erp.app.analytics.trackScreenUsingNavEntry import de.gematik.ti.erp.app.core.LocalAnalytics -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.prescription.ui.model.ScanData 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.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.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedPluralsResource import de.gematik.ti.erp.app.utils.compose.annotatedStringBold import kotlinx.coroutines.Dispatchers @@ -226,15 +226,15 @@ fun ScanScreen( coroutineScope.launch { scanPrescriptionController.saveToDatabase() tracker.trackSaveScannedPrescriptions() - mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) } + mainNavController.navigate(MainNavigationScreens.Prescriptions.path()) }, onClickRedeem = { coroutineScope.launch { scanPrescriptionController.saveToDatabase() tracker.trackSaveScannedPrescriptions() - mainNavController.navigate(MainNavigationScreens.Redeem.path()) } + mainNavController.navigate(MainNavigationScreens.Redeem.path()) } ) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt index c1a9deb3..27fbf913 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/StatusChip.kt @@ -49,8 +49,8 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.SpacerSmall diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeProcessor.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeScanner.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/TwoDCodeValidator.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/PrescriptionScreenData.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/ScanData.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/ui/model/SentOrCompleted.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt new file mode 100644 index 00000000..048cf4fc --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GeneratePrescriptionDetailsUseCase.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge + +class GeneratePrescriptionDetailsUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(taskId: String): Flow { + val synced = repository + .loadSyncedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Synced) + .flowOn(dispatcher) + + val scanned = repository + .loadScannedTaskByTaskId(taskId) + .mapNotNull { it } + .map(PrescriptionData::Scanned) + .flowOn(dispatcher) + + // We functionally know that + return merge(synced, scanned).flowOn(dispatcher) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetActivePrescriptionsUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/GetArchivedPrescriptionsUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt index b9b25aa4..b6a911d3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt @@ -21,13 +21,14 @@ package de.gematik.ti.erp.app.prescription.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.prescription.detail.ui.model.PrescriptionData import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.TaskRepository import de.gematik.ti.erp.app.prescription.ui.TwoDCodeValidator import de.gematik.ti.erp.app.prescription.ui.ValidScannedCode -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData -import de.gematik.ti.erp.app.prescription.repository.TaskRepository import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import io.github.aakira.napier.Napier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -35,7 +36,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -import io.github.aakira.napier.Napier import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -156,10 +156,12 @@ class PrescriptionUseCase( suspend fun saveScannedCodes(profileId: ProfileIdentifier, scannedCodes: List) { val tasks = scannedCodes.flatMap { code -> - code.extract().map { (_, taskId, accessCode) -> + code.extract().mapIndexed { index, (_, taskId, accessCode) -> ScannedTaskData.ScannedTask( - profileId = "", + profileId = profileId, taskId = taskId, + index = index, + name = null, accessCode = accessCode, scannedOn = code.raw.scannedOn, redeemedOn = null @@ -175,10 +177,10 @@ class PrescriptionUseCase( } fun scannedTasks(profileId: ProfileIdentifier): Flow> = - repository.scannedTasks(profileId).flowOn(dispatchers.IO) + repository.scannedTasks(profileId).flowOn(dispatchers.io) fun syncedTasks(profileId: ProfileIdentifier): Flow> = - repository.syncedTasks(profileId).flowOn(dispatchers.IO) + repository.syncedTasks(profileId).flowOn(dispatchers.io) suspend fun downloadTasks(profileId: ProfileIdentifier): Result = taskRepository.downloadTasks(profileId) @@ -199,7 +201,7 @@ class PrescriptionUseCase( } else { emit(PrescriptionData.Synced(task = task)) } - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) suspend fun deletePrescription(profileId: ProfileIdentifier, taskId: String): Result { return repository.deleteTaskByTaskId(profileId, taskId) diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt index 64518fcd..a863b092 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/RefreshPrescriptionUseCase.kt @@ -44,7 +44,7 @@ class RefreshPrescriptionUseCase( val forProfileId: ProfileIdentifier ) - private val scope = CoroutineScope(dispatchers.IO) + private val scope = CoroutineScope(dispatchers.io) private val requestChannel = Channel(onUndeliveredElement = { it.resultChannel.close(CancellationException()) }) diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt new file mode 100644 index 00000000..fe50e3ae --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/UpdateScannedTaskNameUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.usecase + +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class UpdateScannedTaskNameUseCase( + private val repository: PrescriptionRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke( + taskId: String, + name: String + ) = withContext(dispatcher) { + repository.updateScannedTaskName(taskId, name) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt index 40df77b0..c443b79a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/Prescription.kt @@ -19,7 +19,7 @@ package de.gematik.ti.erp.app.prescription.usecase.model import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 +import de.gematik.ti.erp.app.prescription.model.Communication import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import kotlinx.datetime.Instant @@ -64,7 +64,9 @@ sealed interface Prescription { override val taskId: String, override val redeemedOn: Instant?, val scannedOn: Instant, - val communications: List + val name: String?, + val index: Int, + val communications: List ) : Prescription { override val startedOn = scannedOn } diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt index 0b759571..0f24a6ae 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/prescription/usecase/model/PrescriptionUseCaseData.kt @@ -19,7 +19,7 @@ package de.gematik.ti.erp.app.prescription.usecase.model import androidx.compose.runtime.Immutable -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 +import de.gematik.ti.erp.app.prescription.model.Communication import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import kotlinx.datetime.Instant @@ -65,7 +65,7 @@ object PrescriptionUseCaseData { override val taskId: String, val scannedOn: Instant, override val redeemedOn: Instant?, - val communications: List + val communications: List ) : Prescription() fun redeemedOrExpiredOn(): Instant = diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt new file mode 100644 index 00000000..d73cae40 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ProfilesModule.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles + +import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.DecryptAccessTokenUseCase +import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetSelectedProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.ProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.ResetProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import org.kodein.di.DI +import org.kodein.di.bindProvider +import org.kodein.di.instance + +val profilesModule = DI.Module("profilesModule") { + bindProvider { AddProfileUseCase(instance()) } + bindProvider { DeleteProfileUseCase(instance(), instance()) } + bindProvider { GetActiveProfileUseCase(instance()) } + bindProvider { GetProfilesUseCase(instance()) } + bindProvider { ResetProfileUseCase(instance(), instance()) } + bindProvider { SwitchActiveProfileUseCase(instance()) } + bindProvider { UpdateProfileUseCase(instance()) } + bindProvider { DecryptAccessTokenUseCase(instance()) } + bindProvider { LogoutProfileUseCase(instance()) } + bindProvider { SwitchProfileToPKVUseCase(instance()) } + bindProvider { GetSelectedProfileUseCase(instance()) } + + bindProvider { ProfilesWithPairedDevicesUseCase(instance(), instance()) } + bindProvider { ProfilesUseCase(instance(), instance()) } +} + +val profileRepositoryModule = DI.Module("profileRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultProfilesRepository(instance(), instance()) } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt new file mode 100644 index 00000000..f15c5c98 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/ProfileController.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +@file:Suppress("TooManyFunctions") + +package de.gematik.ti.erp.app.profiles.presentation + +import android.graphics.Bitmap +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.AddProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.DecryptAccessTokenUseCase +import de.gematik.ti.erp.app.profiles.usecase.DeleteProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetProfilesUseCase +import de.gematik.ti.erp.app.profiles.usecase.LogoutProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.ProfilesWithPairedDevicesUseCase +import de.gematik.ti.erp.app.profiles.usecase.ResetProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.SwitchProfileToPKVUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.kodein.di.compose.rememberInstance + +class ProfilesController( + private val addProfileUseCase: AddProfileUseCase, + private val decryptAccessTokenUseCase: DecryptAccessTokenUseCase, + private val deleteProfileUseCase: DeleteProfileUseCase, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val getProfilesUseCase: GetProfilesUseCase, + private val resetProfileUseCase: ResetProfileUseCase, + private val switchActiveProfileUseCase: SwitchActiveProfileUseCase, + private val updateProfileUseCase: UpdateProfileUseCase, + private val logoutProfileUseCase: LogoutProfileUseCase, + private val switchProfileToPKVUseCase: SwitchProfileToPKVUseCase, + private val profilesWithPairedDevicesUseCase: ProfilesWithPairedDevicesUseCase, + private val scope: CoroutineScope +) { + + private val profiles by lazy { + getProfilesUseCase().stateIn(scope, SharingStarted.Lazily, listOf(DEFAULT_EMPTY_PROFILE)) + } + + private val activeProfile by lazy { + getActiveProfileUseCase().stateIn(scope, SharingStarted.Lazily, DEFAULT_EMPTY_PROFILE) + } + + @Composable + fun decryptedAccessToken(profile: Profile) = + decryptAccessTokenUseCase(profile.id).collectAsStateWithLifecycle(null) + + fun pairedDevices(profileId: ProfileIdentifier) = + profilesWithPairedDevicesUseCase.pairedDevices(profileId) + + suspend fun deletePairedDevice(profileId: ProfileIdentifier, device: PairedDevice) = + profilesWithPairedDevicesUseCase.deletePairedDevices(profileId, device) + + @Requirement( + "O.Tokn_6#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "invalidate config and token " + ) + fun logout(profile: Profile) { + scope.launch { + logoutProfileUseCase(profile.id) + } + } + + fun addProfile(profileName: String) { + scope.launch { + addProfileUseCase(profileName) + } + } + + fun removeProfile(profile: Profile, profileName: String?) { + scope.launch { + when (profileName != null) { + true -> resetProfileUseCase(profile, profileName) + false -> deleteProfileUseCase(profile) + } + } + } + + @Composable + fun getProfilesState() = profiles.collectAsStateWithLifecycle() + + @Composable + fun getActiveProfileState() = activeProfile.collectAsStateWithLifecycle() + + fun switchActiveProfile(id: ProfileIdentifier) { + scope.launch { + switchActiveProfileUseCase(id) + } + } + + fun switchToPrivateInsurance(profileId: ProfileIdentifier) { + scope.launch { + switchProfileToPKVUseCase(profileId) + } + } + + fun updateProfileColor(profile: Profile, color: ProfilesData.ProfileColorNames) { + scope.launch { + updateProfileUseCase(modifier = ProfileModifier.Color(color), id = profile.id) + } + } + + fun savePersonalizedProfileImage(profileId: ProfileIdentifier, image: Bitmap) { + scope.launch { + updateProfileUseCase(modifier = ProfileModifier.Image(image), id = profileId) + } + } + + fun updateProfileName(profileId: ProfileIdentifier, name: String) { + scope.launch { + updateProfileUseCase(modifier = ProfileModifier.Name(name), id = profileId) + } + } + + fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { + scope.launch { + updateProfileUseCase(modifier = ProfileModifier.Avatar(avatar), id = profileId) + } + } + + fun clearPersonalizedImage(profileId: ProfileIdentifier) { + scope.launch { + updateProfileUseCase(modifier = ProfileModifier.ClearImage, id = profileId) + } + } + + companion object { + val DEFAULT_EMPTY_PROFILE = Profile( + id = "no-id", + name = "no-name", + insurance = ProfileInsuranceInformation( + insuranceType = ProfilesUseCaseData.InsuranceType.NONE + ), + active = false, + color = ProfilesData.ProfileColorNames.SPRING_GRAY, + lastAuthenticated = null, + ssoTokenScope = null, + avatar = ProfilesData.Avatar.PersonalizedImage + ) + } +} + +@Composable +fun rememberProfilesController(): ProfilesController { + val addProfileUseCase by rememberInstance() + val decryptAccessTokenUseCase by rememberInstance() + val deleteProfileUseCase by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val getProfilesUseCase by rememberInstance() + val resetProfileUseCase by rememberInstance() + val switchActiveProfileUseCase by rememberInstance() + val updateProfileUseCase by rememberInstance() + val logoutProfileUseCase by rememberInstance() + val switchProfileToPKVUseCase by rememberInstance() + val profilesWithPairedDevicesUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + return remember { + ProfilesController( + addProfileUseCase = addProfileUseCase, + decryptAccessTokenUseCase = decryptAccessTokenUseCase, + deleteProfileUseCase = deleteProfileUseCase, + getActiveProfileUseCase = getActiveProfileUseCase, + getProfilesUseCase = getProfilesUseCase, + resetProfileUseCase = resetProfileUseCase, + switchActiveProfileUseCase = switchActiveProfileUseCase, + updateProfileUseCase = updateProfileUseCase, + logoutProfileUseCase = logoutProfileUseCase, + switchProfileToPKVUseCase = switchProfileToPKVUseCase, + profilesWithPairedDevicesUseCase = profilesWithPairedDevicesUseCase, + scope = scope + ) + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/SelectedProfileController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/SelectedProfileController.kt new file mode 100644 index 00000000..aea72425 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/presentation/SelectedProfileController.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.presentation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController.Companion.DEFAULT_EMPTY_PROFILE +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.GetActiveProfileUseCase +import de.gematik.ti.erp.app.profiles.usecase.GetSelectedProfileUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import org.kodein.di.compose.rememberInstance + +class SelectedProfileController( + private val profileId: ProfileIdentifier?, + private val getSelectedProfile: GetSelectedProfileUseCase, + private val getActiveProfileUseCase: GetActiveProfileUseCase, + private val scope: CoroutineScope +) { + + private val selectedProfile by lazy { + profileId?.let { + getSelectedProfile(id = it).stateIn(scope, SharingStarted.Lazily, DEFAULT_EMPTY_PROFILE) + } ?: run { MutableStateFlow(DEFAULT_EMPTY_PROFILE) } + } + + private val activeProfile by lazy { + getActiveProfileUseCase().stateIn(scope, SharingStarted.Lazily, DEFAULT_EMPTY_PROFILE) + } + + val selectedProfileState + @Composable + get() = selectedProfile.collectAsStateWithLifecycle() + + val activeProfileState + @Composable + get() = activeProfile.collectAsStateWithLifecycle() +} + +@Composable +fun rememberSelectedProfileController( + profileId: ProfileIdentifier? = null +): SelectedProfileController { + val getSelectedProfile by rememberInstance() + val getActiveProfileUseCase by rememberInstance() + val scope = rememberCoroutineScope() + + return remember { + SelectedProfileController( + profileId = profileId, + getSelectedProfile = getSelectedProfile, + getActiveProfileUseCase = getActiveProfileUseCase, + scope = scope + ) + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt index 1375d8a7..4ee544a3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/Avatar.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/Avatar.kt @@ -46,7 +46,7 @@ import de.gematik.ti.erp.app.theme.PaddingDefaults @Composable fun Avatar( - avatarModifier: Modifier, + modifier: Modifier, emptyIcon: ImageVector, profile: ProfilesUseCaseData.Profile, ssoStatusColor: Color?, @@ -56,7 +56,7 @@ fun Avatar( val currentSelectedColors = profileColor(profileColorNames = profile.color) Box( - modifier = avatarModifier + modifier = modifier .fillMaxSize() .aspectRatio(1f), contentAlignment = Alignment.Center @@ -76,8 +76,8 @@ fun Avatar( ChooseAvatar( profile = profile, emptyIcon = emptyIcon, - iconModifier = iconModifier, - figure = profile.avatarFigure + modifier = iconModifier, + figure = profile.avatar ) } } @@ -116,17 +116,17 @@ fun CircleBox( private fun AvatarPreview() { AppTheme { Avatar( - avatarModifier = Modifier.size(36.dp), + modifier = Modifier.size(36.dp), profile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insurance = de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation( insuranceType = ProfilesUseCaseData.InsuranceType.NONE ), active = false, color = ProfilesData.ProfileColorNames.SUN_DEW, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage, - personalizedImage = null, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null, lastAuthenticated = null, ssoTokenScope = null ), @@ -143,18 +143,18 @@ private fun AvatarPreview() { private fun AvatarWithSSOPreview() { AppTheme { Avatar( - avatarModifier = Modifier.size(36.dp), + modifier = Modifier.size(36.dp), profile = ProfilesUseCaseData.Profile( id = "", name = "", - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insurance = de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation( insuranceType = ProfilesUseCaseData.InsuranceType.NONE ), active = false, color = ProfilesData.ProfileColorNames.SUN_DEW, - avatarFigure = ProfilesData.AvatarFigure.PersonalizedImage, - personalizedImage = null, + avatar = ProfilesData.Avatar.PersonalizedImage, + image = null, lastAuthenticated = null, ssoTokenScope = null ), diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt similarity index 91% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt index d046e4fe..f0350207 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileNavigation.kt @@ -21,7 +21,6 @@ package de.gematik.ti.erp.app.profiles.ui import AuditEventsScreen import TokenScreen import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -34,14 +33,16 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import de.gematik.ti.erp.app.Route -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens import de.gematik.ti.erp.app.pkv.ui.InvoiceDetailsScreen import de.gematik.ti.erp.app.pkv.ui.InvoiceInformationScreen import de.gematik.ti.erp.app.pkv.ui.InvoiceLocalCorrectionScreen import de.gematik.ti.erp.app.pkv.ui.InvoicesScreen import de.gematik.ti.erp.app.pkv.ui.ShareInformationScreen import de.gematik.ti.erp.app.pkv.ui.rememberInvoicesController +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.presentation.rememberSelectedProfileController import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.settings.rememberAuditEventsController import de.gematik.ti.erp.app.utils.compose.NavigationAnimation @@ -93,7 +94,6 @@ object ProfileDestinations { @Suppress("LongMethod") @Composable fun EditProfileNavGraph( - profilesState: ProfilesStateData.ProfilesState, navController: NavHostController, onBack: () -> Unit, selectedProfile: ProfilesUseCaseData.Profile, @@ -102,46 +102,43 @@ fun EditProfileNavGraph( mainNavController: NavController ) { var previousNavEntry by remember { mutableStateOf("profile") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val scope = rememberCoroutineScope() val invoicesController = rememberInvoicesController(profileId = selectedProfile.id) val auditEventsController = rememberAuditEventsController() NavHost(navController = navController, startDestination = ProfileDestinations.Profile.route) { composable(ProfileDestinations.Profile.route) { + val selectedProfileController = rememberSelectedProfileController(selectedProfile.id) + + val profiles by profilesController.getProfilesState() + val profile by selectedProfileController.selectedProfileState + EditProfileScreenContent( + selectedProfile = profile, + profiles = profiles, + profilesController = profilesController, onClickToken = { navController.navigate(ProfileDestinations.Token.path()) }, - onClickAuditEvents = { - navController.navigate(ProfileDestinations.AuditEvents.path()) - }, + onClickAuditEvents = { navController.navigate(ProfileDestinations.AuditEvents.path()) }, onClickLogIn = { - scope.launch { - profilesController.switchActiveProfile(selectedProfile) - } + profilesController.switchActiveProfile(selectedProfile.id) mainNavController.navigate( MainNavigationScreens.CardWall.path(selectedProfile.id) ) }, - onClickLogout = { - scope.launch { - profilesController.logout(selectedProfile) - } - }, - onBack = onBack, - profilesState = profilesState, - profilesController = profilesController, - selectedProfile = selectedProfile, + onClickLogout = { profilesController.logout(selectedProfile) }, + onClickInvoices = { navController.navigate(ProfileDestinations.Invoices.path()) }, onRemoveProfile = onRemoveProfile, onClickEditAvatar = { navController.navigate(ProfileDestinations.ProfileImagePicker.path()) }, onClickPairedDevices = { navController.navigate(ProfileDestinations.PairedDevices.path()) }, - onClickInvoices = { navController.navigate(ProfileDestinations.Invoices.path()) } + onBack = onBack ) } composable(ProfileDestinations.ProfileImagePicker.route) { ProfileColorAndImagePicker( - selectedProfile, + selectedProfile = selectedProfile, clearPersonalizedImage = { scope.launch { profilesController.clearPersonalizedImage(selectedProfile.id) @@ -181,7 +178,7 @@ fun EditProfileNavGraph( } composable(ProfileDestinations.Token.route) { - val accessToken by profilesController.decryptedAccessToken(selectedProfile).collectAsState(null) + val accessToken by profilesController.decryptedAccessToken(selectedProfile) NavigationAnimation(mode = NavigationMode.Closed) { TokenScreen( @@ -196,9 +193,7 @@ fun EditProfileNavGraph( AuditEventsScreen( profileId = selectedProfile.id, onShowCardWall = { - scope.launch { - profilesController.switchActiveProfile(selectedProfile) - } + profilesController.switchActiveProfile(selectedProfile.id) mainNavController.navigate( MainNavigationScreens.CardWall.path(selectedProfile.id) ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt index d5f6da8c..cf2d758f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/EditProfileScreen.kt @@ -100,14 +100,19 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.TestTag.Profile.OpenTokensScreenButton import de.gematik.ti.erp.app.TestTag.Profile.ProfileScreen +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.profileById import de.gematik.ti.erp.app.settings.ui.ProfileNameDialog import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -120,23 +125,21 @@ import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.visualTestTag -import de.gematik.ti.erp.app.utils.sanitizeProfileName +import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName import kotlinx.coroutines.launch import kotlinx.datetime.Instant @Composable fun EditProfileScreen( - profilesState: ProfilesStateData.ProfilesState, profile: ProfilesUseCaseData.Profile, profilesController: ProfilesController, onRemoveProfile: (newProfileName: String?) -> Unit, - onBack: () -> Unit, - mainNavController: NavController + mainNavController: NavController, + onBack: () -> Unit ) { val navController = rememberNavController() EditProfileNavGraph( - profilesState = profilesState, navController = navController, onBack = onBack, selectedProfile = profile, @@ -153,15 +156,14 @@ fun EditProfileScreen( mainNavController: NavController ) { val profilesController = rememberProfilesController() - val profilesState by profilesController.profilesState + val profiles by profilesController.getProfilesState() val scope = rememberCoroutineScope() - profilesState.profileById(profileId)?.let { profile -> + profiles.profileById(profileId)?.let { profile -> val selectedProfile = remember(profile) { profile } EditProfileScreen( - profilesState = profilesState, onBack = onBack, profile = selectedProfile, profilesController = profilesController, @@ -179,9 +181,8 @@ fun EditProfileScreen( @Suppress("LongParameterList") @Composable fun EditProfileScreenContent( - onBack: () -> Unit, selectedProfile: ProfilesUseCaseData.Profile, - profilesState: ProfilesStateData.ProfilesState, + profiles: List, profilesController: ProfilesController, onRemoveProfile: (newProfileName: String?) -> Unit, onClickEditAvatar: () -> Unit, @@ -190,7 +191,8 @@ fun EditProfileScreenContent( onClickLogout: () -> Unit, onClickAuditEvents: () -> Unit, onClickPairedDevices: () -> Unit, - onClickInvoices: () -> Unit + onClickInvoices: () -> Unit, + onBack: () -> Unit ) { val listState = rememberLazyListState() val scaffoldState = rememberScaffoldState() @@ -202,7 +204,7 @@ fun EditProfileScreenContent( DeleteProfileDialog( onCancel = { deleteProfileDialogVisible = false }, onClickAction = { - if (profilesState.profiles.size == 1) { + if (profiles.size == 1) { showAddDefaultProfileDialog = true } else { onRemoveProfile(null) @@ -238,7 +240,7 @@ fun EditProfileScreenContent( item { ProfileNameSection( profile = selectedProfile, - profilesState = profilesState, + profiles = profiles, onChangeProfileName = { scope.launch { profilesController.updateProfileName(selectedProfile.id, it) @@ -257,12 +259,12 @@ fun EditProfileScreenContent( ProfileInsuranceInformation( selectedProfile.lastAuthenticated, selectedProfile.ssoTokenScope, - selectedProfile.insuranceInformation, + selectedProfile.insurance, onClickLogIn ) } - if (selectedProfile.insuranceInformation.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { + if (selectedProfile.insurance.insuranceType == ProfilesUseCaseData.InsuranceType.PKV) { item { ProfileInvoiceInformation { onClickInvoices() } } @@ -503,7 +505,7 @@ fun SettingsMenuHeadline(text: String) { @Composable fun ProfileNameSection( profile: ProfilesUseCaseData.Profile, - profilesState: ProfilesStateData.ProfilesState, + profiles: List, onChangeProfileName: (String) -> Unit ) { var profileName by remember(profile.name) { mutableStateOf(profile.name) } @@ -565,7 +567,7 @@ fun ProfileNameSection( profileName = name profileNameValid = isValid }, - profilesState = profilesState, + profiles = profiles, onDone = { if (profileNameValid) { onChangeProfileName(profileName) @@ -602,7 +604,7 @@ fun ProfileEditBasicTextField( textStyle: TextStyle = AppTheme.typography.h5, initialProfileName: String, onChangeProfileName: (String, Boolean) -> Unit, - profilesState: ProfilesStateData.ProfilesState, + profiles: List, onDone: () -> Unit ) { var profileNameState by remember { @@ -630,7 +632,7 @@ fun ProfileEditBasicTextField( onChangeProfileName( name, name.trim().equals(initialProfileName, true) || - !profilesState.containsProfileWithName(name) && name.isNotEmpty() + !profiles.containsProfileWithName(name) && name.isNotEmpty() ) }, enabled = enabled, @@ -675,9 +677,9 @@ fun ProfileAvatarSection( ) { ChooseAvatar( emptyIcon = Icons.Rounded.AddAPhoto, - iconModifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp), profile = profile, - figure = profile.avatarFigure + figure = profile.avatar ) } } @@ -695,24 +697,24 @@ fun ProfileAvatarSection( @Composable fun ChooseAvatar( - iconModifier: Modifier = Modifier, + modifier: Modifier = Modifier, useSmallImages: Boolean? = false, profile: ProfilesUseCaseData.Profile, emptyIcon: ImageVector, showPersonalizedImage: Boolean = true, - figure: ProfilesData.AvatarFigure + figure: ProfilesData.Avatar ) { - val imageRessource = extractImageResource(useSmallImages, figure) + val imageResource = extractImageResource(useSmallImages, figure) when (figure) { - ProfilesData.AvatarFigure.PersonalizedImage -> { + ProfilesData.Avatar.PersonalizedImage -> { if (showPersonalizedImage) { - if (profile.personalizedImage != null) { + if (profile.image != null) { BitmapImage(profile) } else { Icon( emptyIcon, - modifier = iconModifier, + modifier = modifier, contentDescription = null, tint = AppTheme.colors.neutral600 ) @@ -721,16 +723,16 @@ fun ChooseAvatar( } else -> { - if (imageRessource == 0) { + if (imageResource == 0) { Icon( emptyIcon, - modifier = iconModifier, + modifier = modifier, contentDescription = null, tint = AppTheme.colors.neutral600 ) } else { Image( - painterResource(id = imageRessource), + painterResource(id = imageResource), null, modifier = Modifier.fillMaxSize() ) @@ -742,39 +744,39 @@ fun ChooseAvatar( @Composable private fun extractImageResource( useSmallImages: Boolean? = false, - figure: ProfilesData.AvatarFigure + figure: ProfilesData.Avatar ) = if (useSmallImages == true) { when (figure) { - ProfilesData.AvatarFigure.FemaleDoctor -> R.drawable.femal_doctor_small_portrait - ProfilesData.AvatarFigure.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_small_portrait - ProfilesData.AvatarFigure.Grandfather -> R.drawable.grand_father_small_portrait - ProfilesData.AvatarFigure.BoyWithHealthCard -> R.drawable.boy_with_health_card_small_portrait - ProfilesData.AvatarFigure.OldManOfColor -> R.drawable.old_man_of_color_small_portrait - ProfilesData.AvatarFigure.WomanWithPhone -> R.drawable.woman_with_phone_small_portrait - ProfilesData.AvatarFigure.Grandmother -> R.drawable.grand_mother_small_portrait - ProfilesData.AvatarFigure.ManWithPhone -> R.drawable.man_with_phone_small_portrait - ProfilesData.AvatarFigure.WheelchairUser -> R.drawable.wheel_chair_user_small_portrait - ProfilesData.AvatarFigure.Baby -> R.drawable.baby_small_portrait - ProfilesData.AvatarFigure.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_small_portrait - ProfilesData.AvatarFigure.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_small_portrait - ProfilesData.AvatarFigure.FemaleDeveloper -> R.drawable.femal_developer_small_portrait + ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_small_portrait + ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_small_portrait + ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_small_portrait + ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_small_portrait + ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_small_portrait + ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_small_portrait + ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_small_portrait + ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_small_portrait + ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_small_portrait + ProfilesData.Avatar.Baby -> R.drawable.baby_small_portrait + ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_small_portrait + ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_small_portrait + ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_small_portrait else -> 0 } } else { when (figure) { - ProfilesData.AvatarFigure.FemaleDoctor -> R.drawable.femal_doctor_portrait - ProfilesData.AvatarFigure.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_portrait - ProfilesData.AvatarFigure.Grandfather -> R.drawable.grand_father_portrait - ProfilesData.AvatarFigure.BoyWithHealthCard -> R.drawable.boy_with_health_card_portrait - ProfilesData.AvatarFigure.OldManOfColor -> R.drawable.old_man_of_color_portrait - ProfilesData.AvatarFigure.WomanWithPhone -> R.drawable.woman_with_phone_portrait - ProfilesData.AvatarFigure.Grandmother -> R.drawable.grand_mother_portrait - ProfilesData.AvatarFigure.ManWithPhone -> R.drawable.man_with_phone_portrait - ProfilesData.AvatarFigure.WheelchairUser -> R.drawable.wheel_chair_user_portrait - ProfilesData.AvatarFigure.Baby -> R.drawable.baby_portrait - ProfilesData.AvatarFigure.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_portrait - ProfilesData.AvatarFigure.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_portrait - ProfilesData.AvatarFigure.FemaleDeveloper -> R.drawable.femal_developer_portrait + ProfilesData.Avatar.FemaleDoctor -> R.drawable.femal_doctor_portrait + ProfilesData.Avatar.WomanWithHeadScarf -> R.drawable.woman_with_head_scarf_portrait + ProfilesData.Avatar.Grandfather -> R.drawable.grand_father_portrait + ProfilesData.Avatar.BoyWithHealthCard -> R.drawable.boy_with_health_card_portrait + ProfilesData.Avatar.OldManOfColor -> R.drawable.old_man_of_color_portrait + ProfilesData.Avatar.WomanWithPhone -> R.drawable.woman_with_phone_portrait + ProfilesData.Avatar.Grandmother -> R.drawable.grand_mother_portrait + ProfilesData.Avatar.ManWithPhone -> R.drawable.man_with_phone_portrait + ProfilesData.Avatar.WheelchairUser -> R.drawable.wheel_chair_user_portrait + ProfilesData.Avatar.Baby -> R.drawable.baby_portrait + ProfilesData.Avatar.MaleDoctorWithPhone -> R.drawable.doctor_with_phone_portrait + ProfilesData.Avatar.FemaleDoctorWithPhone -> R.drawable.femal_doctor_with_phone_portrait + ProfilesData.Avatar.FemaleDeveloper -> R.drawable.femal_developer_portrait else -> 0 } } @@ -782,8 +784,8 @@ private fun extractImageResource( @Composable fun BitmapImage(profile: ProfilesUseCaseData.Profile) { val bitmap by produceState(initialValue = null, profile) { - value = profile.personalizedImage?.let { - BitmapFactory.decodeByteArray(profile.personalizedImage, 0, it.size).asImageBitmap() + value = profile.image?.let { + BitmapFactory.decodeByteArray(profile.image, 0, it.size).asImageBitmap() } } @@ -800,7 +802,7 @@ fun BitmapImage(profile: ProfilesUseCaseData.Profile) { fun ProfileInsuranceInformation( lastAuthenticated: Instant?, ssoTokenScope: IdpData.SingleSignOnTokenScope?, - insuranceInformation: ProfilesUseCaseData.ProfileInsuranceInformation, + insuranceInformation: ProfileInsuranceInformation, onClickLogIn: () -> Unit ) { SpacerLarge() diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt similarity index 96% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt index 807b9288..3f37561a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/PairedDevices.kt @@ -57,13 +57,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.NoneEnrolledException -import de.gematik.ti.erp.app.cardwall.mini.ui.PromptAuthenticator import de.gematik.ti.erp.app.cardwall.mini.ui.UserNotAuthenticatedException import de.gematik.ti.erp.app.core.LocalAuthenticator +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.usecase.RefreshFlowException +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults @@ -76,6 +78,7 @@ import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.annotatedStringBold import de.gematik.ti.erp.app.utils.compose.annotatedStringResource import de.gematik.ti.erp.app.utils.compose.toAnnotatedString +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest @@ -83,13 +86,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.retry import kotlinx.coroutines.launch -import io.github.aakira.napier.Napier -import java.io.IOException -import java.net.UnknownHostException import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime +import java.io.IOException +import java.net.UnknownHostException import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -160,7 +162,7 @@ private sealed interface RefreshState { @Stable private sealed interface DeleteState { @Stable - class Deleting(val device: ProfilesUseCaseData.PairedDevice, val isThisDevice: Boolean) : DeleteState + class Deleting(val device: PairedDevice, val isThisDevice: Boolean) : DeleteState @Stable object None : DeleteState @@ -190,7 +192,7 @@ private fun PairedDevices( .pairedDevices(selectedProfile.id) .retry(1) { throwable -> Napier.e("Couldn't get paired devices", throwable) - if (throwable is RefreshFlowException && throwable.userActionRequired) { + if (throwable is RefreshFlowException && throwable.isUserAction) { authenticator .authenticateForPairedDevices(selectedProfile.id) .first() @@ -200,6 +202,7 @@ private fun PairedDevices( PromptAuthenticator.AuthResult.Cancelled -> false PromptAuthenticator.AuthResult.NoneEnrolled -> throw NoneEnrolledException() + PromptAuthenticator.AuthResult.UserNotAuthenticated -> throw UserNotAuthenticatedException() } @@ -280,8 +283,10 @@ private fun PairedDevices( } ) } + is RefreshState.WithResults -> { - items((state as RefreshState.WithResults).result.devices) { device: ProfilesUseCaseData.PairedDevice -> + val devices = (state as RefreshState.WithResults).result.devices + items(items = devices) { device -> val isThisDevice = keyStoreAlias?.let { device.isOurDevice(it) } ?: false @@ -301,7 +306,7 @@ private fun PairedDevices( @Composable private fun DeleteDeviceDialog( - device: ProfilesUseCaseData.PairedDevice, + device: PairedDevice, isThisDevice: Boolean, onCancel: () -> Unit, onClickAction: () -> Unit @@ -323,7 +328,7 @@ private fun DeleteDeviceDialog( @Composable private fun DeleteOtherDeviceDialog( - device: ProfilesUseCaseData.PairedDevice, + device: PairedDevice, onCancel: () -> Unit, onClickAction: () -> Unit ) { @@ -339,7 +344,7 @@ private fun DeleteOtherDeviceDialog( @Composable private fun DeleteThisDeviceDialog( - device: ProfilesUseCaseData.PairedDevice, + device: PairedDevice, onCancel: () -> Unit, onClickAction: () -> Unit ) { @@ -431,7 +436,7 @@ private fun EmptyScreen( @Composable private fun PairedDevice( - device: ProfilesUseCaseData.PairedDevice, + device: PairedDevice, isOurDevice: Boolean, onDeleteDevice: () -> Unit ) { @@ -476,6 +481,7 @@ fun errorMessageFromException(t: Throwable): Pair { t.cause is UnknownHostException -> network else -> other } + else -> other } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt similarity index 84% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt index 88df4022..7c1bedb2 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColorAndImagePicker.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.profiles.ui -import android.annotation.SuppressLint import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -31,6 +30,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -45,22 +45,24 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AddAPhoto import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.PersonOutline import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp - -import androidx.compose.foundation.layout.imePadding -import androidx.compose.material.icons.rounded.AddAPhoto -import androidx.compose.material.icons.rounded.PersonOutline import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription -import de.gematik.ti.erp.app.R +import androidx.compose.ui.unit.dp import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme @@ -71,17 +73,17 @@ import de.gematik.ti.erp.app.utils.compose.SpacerLarge import de.gematik.ti.erp.app.utils.compose.SpacerMedium import de.gematik.ti.erp.app.utils.compose.SpacerXXLarge -@SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable fun ProfileColorAndImagePicker( selectedProfile: ProfilesUseCaseData.Profile, + onSelectAvatar: (ProfilesData.Avatar) -> Unit, + onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit, clearPersonalizedImage: () -> Unit, onPickPersonalizedImage: () -> Unit, - onBack: () -> Unit, - onSelectAvatar: (ProfilesData.AvatarFigure) -> Unit, - onSelectProfileColor: (ProfilesData.ProfileColorNames) -> Unit + onBack: () -> Unit ) { val listState = rememberLazyListState() + var editableProfile by remember { mutableStateOf(selectedProfile) } Scaffold( modifier = Modifier.imePadding(), @@ -97,26 +99,31 @@ fun ProfileColorAndImagePicker( LazyColumn( state = listState, modifier = Modifier + .padding(it) .fillMaxSize(), contentPadding = PaddingValues(PaddingDefaults.Medium) ) { item { SpacerMedium() - ProfileImage(selectedProfile) { + ProfileImage(editableProfile) { + editableProfile = editableProfile.copy(image = null) clearPersonalizedImage() } } item { SpacerXXLarge() AvatarPicker( - profile = selectedProfile, - currentAvatarFigure = selectedProfile.avatarFigure, + profile = editableProfile, + currentAvatar = editableProfile.avatar, onPickPersonalizedImage = onPickPersonalizedImage, - onSelectAvatar = onSelectAvatar + onSelectAvatar = { + editableProfile = editableProfile.copy(avatar = it) + onSelectAvatar(it) + } ) } - if (selectedProfile.avatarFigure != ProfilesData.AvatarFigure.PersonalizedImage) { + if (editableProfile.avatar != ProfilesData.Avatar.PersonalizedImage) { item { SpacerXXLarge() SpacerMedium() @@ -125,7 +132,13 @@ fun ProfileColorAndImagePicker( style = AppTheme.typography.h6 ) SpacerLarge() - ColorPicker(selectedProfile.color, onSelectProfileColor) + ColorPicker( + profileColorName = editableProfile.color, + onSelectProfileColor = { + editableProfile = editableProfile.copy(color = it) + onSelectProfileColor(it) + } + ) } } } @@ -135,9 +148,9 @@ fun ProfileColorAndImagePicker( @Composable fun AvatarPicker( profile: ProfilesUseCaseData.Profile, - currentAvatarFigure: ProfilesData.AvatarFigure, + currentAvatar: ProfilesData.Avatar, onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.AvatarFigure) -> Unit + onSelectAvatar: (ProfilesData.Avatar) -> Unit ) { val listState = rememberLazyListState() @@ -145,12 +158,12 @@ fun AvatarPicker( state = listState, horizontalArrangement = Arrangement.spacedBy(PaddingDefaults.Medium) ) { - ProfilesData.AvatarFigure.values().forEach { figure -> + ProfilesData.Avatar.values().forEach { figure -> item { AvatarSelector( figure = figure, profile = profile, - selected = figure == currentAvatarFigure, + selected = figure == currentAvatar, onPickPersonalizedImage = onPickPersonalizedImage, onSelectAvatar = onSelectAvatar ) @@ -162,10 +175,10 @@ fun AvatarPicker( @Composable fun AvatarSelector( profile: ProfilesUseCaseData.Profile, - figure: ProfilesData.AvatarFigure, + figure: ProfilesData.Avatar, selected: Boolean, onPickPersonalizedImage: () -> Unit, - onSelectAvatar: (ProfilesData.AvatarFigure) -> Unit + onSelectAvatar: (ProfilesData.Avatar) -> Unit ) { Surface( modifier = Modifier @@ -173,7 +186,7 @@ fun AvatarSelector( shape = CircleShape, border = if (selected) { BorderStroke(5.dp, color = AppTheme.colors.primary600) - } else if (figure != ProfilesData.AvatarFigure.PersonalizedImage) { + } else if (figure != ProfilesData.Avatar.PersonalizedImage) { BorderStroke(1.dp, color = AppTheme.colors.neutral300) } else { null @@ -183,7 +196,7 @@ fun AvatarSelector( modifier = Modifier .background( color = when (figure) { - ProfilesData.AvatarFigure.PersonalizedImage -> { + ProfilesData.Avatar.PersonalizedImage -> { AppTheme.colors.neutral100 } @@ -193,7 +206,7 @@ fun AvatarSelector( } ) .clickable(onClick = { - if (figure == ProfilesData.AvatarFigure.PersonalizedImage) { + if (figure == ProfilesData.Avatar.PersonalizedImage) { onPickPersonalizedImage() onSelectAvatar(figure) } @@ -205,7 +218,7 @@ fun AvatarSelector( ChooseAvatar( useSmallImages = true, emptyIcon = Icons.Rounded.AddAPhoto, - iconModifier = Modifier.size(24.dp), + modifier = Modifier.size(24.dp), profile = profile, figure = figure ) @@ -232,12 +245,16 @@ fun ColorPicker( when (it) { ProfilesData.ProfileColorNames.SPRING_GRAY -> TestTag.Profile.EditProfileIcon.ColorSelectorSpringGrayButton + ProfilesData.ProfileColorNames.SUN_DEW -> TestTag.Profile.EditProfileIcon.ColorSelectorSunDewButton + ProfilesData.ProfileColorNames.PINK -> TestTag.Profile.EditProfileIcon.ColorSelectorPinkButton + ProfilesData.ProfileColorNames.TREE -> TestTag.Profile.EditProfileIcon.ColorSelectorTreeButton + ProfilesData.ProfileColorNames.BLUE_MOON -> TestTag.Profile.EditProfileIcon.ColorSelectorBlueMoonButton } @@ -280,11 +297,11 @@ fun ProfileImage(selectedProfile: ProfilesUseCaseData.Profile, onClickDeleteAvat contentAlignment = Alignment.Center ) { ChooseAvatar( - iconModifier = Modifier.size(36.dp), + modifier = Modifier.size(36.dp), profile = selectedProfile, emptyIcon = Icons.Rounded.PersonOutline, - figure = selectedProfile.avatarFigure, - showPersonalizedImage = selectedProfile.personalizedImage != null + figure = selectedProfile.avatar, + showPersonalizedImage = selectedProfile.image != null ) } if (!(selectedProfile.hasNoImageSelected())) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt index 0ba2e263..2a3bc445 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileColors.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.theme.AppTheme diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt new file mode 100644 index 00000000..27cf28fd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileHandler.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.ui + +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile + +@Deprecated("Not to be used, left here for reference in case of errors found") +class ProfileHandler { + + enum class ProfileConnectionState { + LoggedIn, + LoggedOutWithoutTokenBiometrics, + LoggedOutWithoutToken, + LoggedOut, + NeverConnected + } + + private fun Profile.neverConnected() = ssoTokenScope == null && lastAuthenticated == null + + private fun Profile.ssoTokenSetAndConnected() = + ssoTokenScope?.token != null && ssoTokenScope?.token?.isValid() == true + + private fun Profile.ssoTokenSetAndDisconnected() = + ssoTokenScope != null && ssoTokenScope?.token?.isValid() == false || + lastAuthenticated != null + + private fun Profile.ssoTokenNotSet() = + when (ssoTokenScope) { + is IdpData.ExternalAuthenticationToken, + is IdpData.AlternateAuthenticationToken, + is IdpData.AlternateAuthenticationWithoutToken, + is IdpData.DefaultToken -> ssoTokenScope?.token == null + + null -> true + } + + private fun Profile.ssoTokenWithoutScope() = + when (ssoTokenScope) { + is IdpData.AlternateAuthenticationWithoutToken -> true + else -> false + } + + @Stable + fun connectionState(profile: Profile): ProfileConnectionState? = + when { + profile.neverConnected() -> + ProfileConnectionState.NeverConnected + + profile.ssoTokenWithoutScope() -> + ProfileConnectionState.LoggedOutWithoutTokenBiometrics + + profile.ssoTokenNotSet() -> + ProfileConnectionState.LoggedOutWithoutToken + + profile.ssoTokenSetAndConnected() -> + ProfileConnectionState.LoggedIn + + profile.ssoTokenSetAndDisconnected() -> + ProfileConnectionState.LoggedOut + + else -> null + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt index dcdbff94..7b422254 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileImageCropper.kt @@ -30,16 +30,17 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.blur @@ -53,8 +54,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.updateLayoutParams import com.canhub.cropper.CropImageView -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar @@ -110,7 +111,7 @@ fun ProfileImageCropper(onSaveCroppedImage: (Bitmap) -> Unit, onBack: () -> Unit ) }, backgroundColor = Color.Black - ) { + ) { paddingValues -> var background: Bitmap? by remember { mutableStateOf(null) } val imagePickerLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? -> @@ -124,7 +125,11 @@ fun ProfileImageCropper(onSaveCroppedImage: (Bitmap) -> Unit, onBack: () -> Unit imagePickerLauncher.launch("image/*") } - BoxWithConstraints(Modifier.fillMaxSize()) { + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { background?.let { Image( bitmap = it.asImageBitmap(), diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt index f4ec7802..6ad4f058 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/ui/ProfileStringRessource.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.profiles.ui import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.idp.model.IdpData @Composable diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt new file mode 100644 index 00000000..06ad2a37 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/AddProfileUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Adds a new profile with the given name to the [repository] + */ +class AddProfileUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(name: String) { + withContext(dispatcher) { + repository.saveProfile(name, activate = true) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt new file mode 100644 index 00000000..26cd219d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DecryptAccessTokenUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +class DecryptAccessTokenUseCase( + private val repository: IdpRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(id: ProfileIdentifier): Flow = + + repository.decryptedAccessToken(id).mapNotNull { it }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt new file mode 100644 index 00000000..cd3d699c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/DeleteProfileUseCase.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Deletes the profile from the [profileRepository] and invalidates it on the [idpRepository] + */ +class DeleteProfileUseCase( + private val profileRepository: ProfileRepository, + private val idpRepository: IdpRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(profile: Profile) { + withContext(dispatcher) { + idpRepository.invalidateDecryptedAccessToken(profile.id) + profileRepository.removeProfile(profile.id) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt new file mode 100644 index 00000000..30be8b08 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetActiveProfileUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +/** + * Gets the current active profile from the [repository] + */ +class GetActiveProfileUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow = + repository.activeProfile().mapNotNull { it.toModel() }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt new file mode 100644 index 00000000..454e2225 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetProfilesUseCase.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.model.IdpData.AlternateAuthenticationWithoutToken +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach + +/** + * Gets all the profiles from the [repository] + */ +class GetProfilesUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(): Flow> = + repository.profiles().mapNotNull { profiles -> + profiles.map { it.toModel() } + }.distinctUntilChanged() + .onEach { profiles -> + profiles.forEach { profile -> + when { + profile.ssoTokenScope != null && + profile.ssoTokenScope !is AlternateAuthenticationWithoutToken && + profile.lastAuthenticated == null -> { + profile.ssoTokenScope?.token?.let { token -> + repository.updateLastAuthenticated(profile.id, token.validOn) + } + } + } + } + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt new file mode 100644 index 00000000..9262f2dd --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/GetSelectedProfileUseCase.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.mapper.toModel +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +class GetSelectedProfileUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + operator fun invoke(id: ProfileIdentifier): Flow = + repository.profiles().mapNotNull { profiles -> + profiles.find { it.id == id }?.toModel() + }.flowOn(dispatcher) +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt new file mode 100644 index 00000000..bbe9e4a0 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/LogoutProfileUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class LogoutProfileUseCase( + private val repository: IdpRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(id: ProfileIdentifier) { + withContext(dispatcher) { + repository.invalidate(id) + } + } +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt similarity index 91% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt index fd72eb50..85a566e7 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesUseCase.kt @@ -24,7 +24,8 @@ import de.gematik.ti.erp.app.idp.model.IdpData import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.model.ProfilesData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -35,8 +36,9 @@ import kotlinx.coroutines.flow.onEach fun List.activeProfile() = find { profile -> profile.active }!! +// TODO: Used only in test and debug-viewmodel. Remove it from there too. class ProfilesUseCase( - private val profilesRepository: ProfilesRepository, + private val profilesRepository: ProfileRepository, private val idpRepository: IdpRepository ) { @@ -46,7 +48,7 @@ class ProfilesUseCase( ProfilesUseCaseData.Profile( id = profile.id, name = profile.name, - insuranceInformation = ProfilesUseCaseData.ProfileInsuranceInformation( + insurance = ProfileInsuranceInformation( insurantName = profile.insurantName ?: "", insuranceIdentifier = profile.insuranceIdentifier ?: "", insuranceName = profile.insuranceName ?: "", @@ -58,8 +60,8 @@ class ProfilesUseCase( ), active = profile.active, color = profile.color, - avatarFigure = profile.avatarFigure, - personalizedImage = profile.personalizedImage, + avatar = profile.avatar, + image = profile.personalizedImage, lastAuthenticated = profile.lastAuthenticated, ssoTokenScope = profile.singleSignOnTokenScope ) @@ -72,7 +74,7 @@ class ProfilesUseCase( profile.ssoTokenScope !is IdpData.AlternateAuthenticationWithoutToken && profile.lastAuthenticated == null ) { - profile.ssoTokenScope.token?.let { token -> + profile.ssoTokenScope?.token?.let { token -> profilesRepository.updateLastAuthenticated(profile.id, token.validOn) } } @@ -141,6 +143,7 @@ class ProfilesUseCase( profile.active } } + suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { profilesRepository.switchProfileToPKV(profileId) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt index c8845307..9dc5cece 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ProfilesWithPairedDevicesUseCase.kt @@ -21,12 +21,14 @@ package de.gematik.ti.erp.app.profiles.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.idp.usecase.IdpUseCase import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.usecase.model.PairedDevice import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.datetime.Instant +// TODO: Use the IdpUseCase and do this in the controller where it is called or do this in the IdpUseCase class ProfilesWithPairedDevicesUseCase( private val idpUseCase: IdpUseCase, private val dispatchers: DispatchProvider @@ -36,7 +38,7 @@ class ProfilesWithPairedDevicesUseCase( emit( ProfilesUseCaseData.PairedDevices( idpUseCase.getPairedDevices(profileId).getOrThrow().map { (raw, pairingData) -> - ProfilesUseCaseData.PairedDevice( + PairedDevice( name = raw.name, alias = pairingData.keyAliasOfSecureElement, connectedOn = Instant.fromEpochSeconds(raw.creationTime) @@ -46,11 +48,11 @@ class ProfilesWithPairedDevicesUseCase( } ) ) - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) suspend fun deletePairedDevices( profileId: ProfileIdentifier, - device: ProfilesUseCaseData.PairedDevice + device: PairedDevice ): Result = idpUseCase.deletePairedDevice(profileId = profileId, deviceAlias = device.alias).map { device.name diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt new file mode 100644 index 00000000..bc5ad63c --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/ResetProfileUseCase.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Removes the profile from the [profileRepository] and + * invalidates it in the [idpRepository] + * adds a new profile with the new name to the [profileRepository]. + */ +class ResetProfileUseCase( + private val profileRepository: ProfileRepository, + private val idpRepository: IdpRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(profile: Profile, newProfileName: String) { + withContext(dispatcher) { + // Not sure if the order needs to be changed here. + profileRepository.saveProfile(newProfileName, activate = true) + idpRepository.invalidateDecryptedAccessToken(profile.name) + profileRepository.removeProfile(profile.id) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt new file mode 100644 index 00000000..894403aa --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchActiveProfileUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SwitchActiveProfileUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(id: ProfileIdentifier) { + withContext(dispatcher) { + repository.activateProfile(profileId = id) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt new file mode 100644 index 00000000..a8cca15e --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/SwitchProfileToPKVUseCase.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class SwitchProfileToPKVUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(id: ProfileIdentifier) { + withContext(dispatcher) { + repository.switchProfileToPKV(id) + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt new file mode 100644 index 00000000..bea71f5d --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/UpdateProfileUseCase.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase + +import android.graphics.Bitmap +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Avatar +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.ClearImage +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Color +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Image +import de.gematik.ti.erp.app.profiles.usecase.UpdateProfileUseCase.Companion.ProfileModifier.Name +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream + +class UpdateProfileUseCase( + private val repository: ProfileRepository, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + suspend operator fun invoke(modifier: ProfileModifier, id: ProfileIdentifier) { + withContext(dispatcher) { + when (modifier) { + is Avatar -> repository.saveAvatarFigure(id, modifier.value) + is Color -> repository.updateProfileColor(id, modifier.value) + is Image -> repository.savePersonalizedProfileImage(id, modifier.value.toByteArray()) + is Name -> repository.updateProfileName(id, modifier.value) + is ClearImage -> repository.clearPersonalizedProfileImage(id) + } + } + } + + companion object { + private const val BitmapQuality = 100 + private fun Bitmap.toByteArray(): ByteArray { + val outputStream = ByteArrayOutputStream() + compress(Bitmap.CompressFormat.PNG, BitmapQuality, outputStream) + return outputStream.toByteArray() + } + + sealed interface ProfileModifier { + data class Name(val value: String) : ProfileModifier + data class Color(val value: ProfilesData.ProfileColorNames) : ProfileModifier + data class Image(val value: Bitmap) : ProfileModifier + data class Avatar(val value: ProfilesData.Avatar) : ProfileModifier + data object ClearImage : ProfileModifier + } + } +} diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt new file mode 100644 index 00000000..115f5dc7 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/profiles/usecase/mapper/ProfileMapper.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase.mapper + +import de.gematik.ti.erp.app.db.entities.v1.InsuranceTypeV1 +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfileInsuranceInformation +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +fun ProfilesData.Profile.toModel() = + ProfilesUseCaseData.Profile( + id = id, + name = name, + insurance = ProfileInsuranceInformation( + insurantName = insurantName ?: "", + insuranceIdentifier = insuranceIdentifier ?: "", + insuranceName = insuranceName ?: "", + insuranceType = when (insuranceType) { + InsuranceTypeV1.None -> ProfilesUseCaseData.InsuranceType.NONE + InsuranceTypeV1.GKV -> ProfilesUseCaseData.InsuranceType.GKV + InsuranceTypeV1.PKV -> ProfilesUseCaseData.InsuranceType.PKV + } + ), + active = active, + color = color, + avatar = avatar, + image = personalizedImage, + lastAuthenticated = lastAuthenticated, + ssoTokenScope = singleSignOnTokenScope + ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/RedeemModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/RedeemModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/RedeemModule.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt index 04d14266..bd09ab57 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/HowToRedeem.kt @@ -43,7 +43,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.pharmacy.ui.FlatButton import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt index bd9b45a8..1c66c49d 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemScreen.kt @@ -49,23 +49,23 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.rememberPagerState -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.core.LocalActivity -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderState +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.utils.ForceBrightness import de.gematik.ti.erp.app.utils.compose.CommonAlertDialog import de.gematik.ti.erp.app.utils.compose.DataMatrix import de.gematik.ti.erp.app.utils.compose.NavigationBarMode import de.gematik.ti.erp.app.utils.compose.NavigationTopAppBar import de.gematik.ti.erp.app.utils.compose.TertiaryButton import de.gematik.ti.erp.app.utils.compose.createBitMatrix +import de.gematik.ti.erp.app.utils.extensions.forceBrightness import kotlinx.coroutines.launch @Requirement( @@ -73,13 +73,13 @@ import kotlinx.coroutines.launch sourceSpecification = "gemSpec_eRp_FdV", rationale = "Only the title of the prescription and the DMC consisting of Task-ID and Access-Code are displayed." ) -@OptIn(ExperimentalPagerApi::class) @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable fun LocalRedeemScreen( + activeProfile: ProfilesUseCaseData.Profile, + orderState: PharmacyOrderController, onBack: () -> Unit, - onFinished: () -> Unit, - orderState: PharmacyOrderState + onFinished: () -> Unit ) { val localRedeemState = rememberLocalRedeemState(orderState) val codes by localRedeemState.codes @@ -89,13 +89,13 @@ fun LocalRedeemScreen( } val activity = LocalActivity.current - activity.ForceBrightness() + activity.forceBrightness() - val redeemController = rememberRedeemController() + val redeemController = rememberRedeemController(activeProfile) var showRedeemScannedDialog by remember { mutableStateOf(false) } if (showRedeemScannedDialog) { val scope = rememberCoroutineScope() - val prescriptions by orderState.prescriptions + val prescriptions by orderState.prescriptionsState RedeemScannedPrescriptionsDialog( onClickRedeem = { scope.launch { @@ -193,7 +193,6 @@ fun DataMatrix( sourceSpecification = "gemSpec_eRp_FdV", rationale = "User displays a DMC to redeem a prescription in a pharmacy." ) -@OptIn(ExperimentalPagerApi::class) @Composable private fun PageIndicator( modifier: Modifier, diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt index 1b7a31a8..c76834a4 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/LocalRedeemState.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderState +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.utils.createDMPayload @@ -91,9 +91,9 @@ class LocalRedeemState( @Composable fun rememberLocalRedeemState( - orderState: PharmacyOrderState + orderState: PharmacyOrderController ): LocalRedeemState { - val prescriptions = orderState.prescriptions + val prescriptions = orderState.prescriptionsState return remember { LocalRedeemState( prescriptions diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt similarity index 87% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt index 7935fa1f..ffd90c21 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/Navigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/Navigation.kt @@ -26,10 +26,11 @@ import androidx.compose.runtime.setValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChanges 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 +import de.gematik.ti.erp.app.pharmacy.presentation.rememberPharmacyOrderController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.redeem.ui.model.RedeemNavigation import de.gematik.ti.erp.app.utils.compose.NavigationAnimation import de.gematik.ti.erp.app.utils.compose.navigationModeState @@ -38,13 +39,13 @@ import de.gematik.ti.erp.app.utils.compose.navigationModeState fun RedeemNavigation( onFinish: () -> Unit ) { - val orderState = rememberPharmacyOrderState() + val orderState = rememberPharmacyOrderController() val navController = rememberNavController() val navigationMode by navController.navigationModeState(RedeemNavigation.MethodSelection.route) var previousNavEntry by remember { mutableStateOf("redeem_methodSelection") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, @@ -52,7 +53,7 @@ fun RedeemNavigation( ) { composable(RedeemNavigation.MethodSelection.route) { NavigationAnimation(mode = navigationMode) { - val prescriptions by orderState.prescriptions + val prescriptions by orderState.prescriptionsState HowToRedeem( onClickLocalRedeem = { navController.navigate(RedeemNavigation.LocalRedeem.path()) @@ -69,8 +70,11 @@ fun RedeemNavigation( } } composable(RedeemNavigation.LocalRedeem.route) { + val profilesController = rememberProfilesController() + val activeProfile by profilesController.getActiveProfileState() NavigationAnimation(mode = navigationMode) { LocalRedeemScreen( + activeProfile = activeProfile, onBack = { navController.popBackStack() }, @@ -115,7 +119,7 @@ fun RedeemNavigation( composable(RedeemNavigation.PharmacySearch.route) { PharmacyNavigation( isNestedNavigation = true, - orderState = orderState, + pharmacyOrderController = orderState, onBack = { navController.popBackStack() }, diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt index aa817db6..41c7c055 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/OnlineRedeemScreen.kt @@ -34,15 +34,14 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.pharmacy.ui.PharmacyOrderState +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.pharmacy.presentation.PharmacyOrderController import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold @@ -51,15 +50,14 @@ 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 -@OptIn(ExperimentalComposeUiApi::class) @Composable fun OnlineRedeemScreen( - orderState: PharmacyOrderState, + orderState: PharmacyOrderController, onClickSelectPrescriptions: () -> Unit, onClickAllPrescriptions: () -> Unit, onBack: () -> Unit ) { - val prescriptions by orderState.prescriptions + val prescriptions by orderState.prescriptionsState val scrollState = rememberScrollState() val elevated by remember { derivedStateOf { scrollState.value > 0 } diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt index d0d68798..9215ce25 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/RedeemController.kt @@ -20,11 +20,11 @@ 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 +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.ui.LocalProfileHandler +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData import de.gematik.ti.erp.app.redeem.usecase.RedeemUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -44,7 +44,7 @@ class RedeemController( val hasRedeemableTasks @Composable - get() = hasRedeemableTasksFlow.collectAsState(false) + get() = hasRedeemableTasksFlow.collectAsStateWithLifecycle(false) suspend fun redeemScannedTasks(taskIds: List) { useCase.redeemScannedTasks(taskIds) @@ -52,8 +52,9 @@ class RedeemController( } @Composable -fun rememberRedeemController(): RedeemController { - val activeProfile = LocalProfileHandler.current.activeProfile +fun rememberRedeemController( + activeProfile: ProfilesUseCaseData.Profile +): RedeemController { val useCase by rememberInstance() val scope = rememberCoroutineScope() return remember(activeProfile.id) { diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/ui/model/RedeemNavigation.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt index e8ecab94..eeb979af 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/redeem/usecase/RedeemUseCase.kt @@ -47,7 +47,7 @@ class RedeemUseCase( } ) { syncedTasks, scannedTasks -> syncedTasks.isNotEmpty() || scannedTasks.isNotEmpty() - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) suspend fun redeemScannedTasks(taskIds: List) { val redeemedOn = Clock.System.now() diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/AuditEventsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/AuditEventsController.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/settings/AuditEventsController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/AuditEventsController.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/SettingsModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt similarity index 95% rename from android/src/main/java/de/gematik/ti/erp/app/settings/SettingsModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt index 89b70057..3dc15551 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/SettingsModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/SettingsModule.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.settings -import de.gematik.ti.erp.app.di.ApplicationPreferencesTag import de.gematik.ti.erp.app.settings.repository.CardWallRepository import de.gematik.ti.erp.app.settings.repository.SettingsRepository import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase @@ -26,6 +25,8 @@ import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance +const val ApplicationPreferencesTag = "ApplicationPreferences" + val settingsModule = DI.Module("settingsModule") { bindProvider { CardWallRepository(prefs = instance(ApplicationPreferencesTag)) } bindProvider { SettingsRepository(instance(), instance()) } diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/repository/CardWallRepository.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt index 51e00d76..a258f70c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AccessibilitySettingsScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.ui.rememberSettingsController import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold import de.gematik.ti.erp.app.utils.compose.LabeledSwitch diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt index ecf62dfa..1fee88cd 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowAnalyticsScreen.kt @@ -33,9 +33,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt index 76e34fb5..77d9e5b1 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AllowBiometryScreen.kt @@ -20,6 +20,7 @@ package de.gematik.ti.erp.app.settings.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize @@ -27,22 +28,21 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R -import de.gematik.ti.erp.app.onboarding.ui.OnboardingSecureAppMethod -import de.gematik.ti.erp.app.theme.PaddingDefaults -import de.gematik.ti.erp.app.userauthentication.ui.BiometricPrompt -import de.gematik.ti.erp.app.utils.compose.NavigationBarMode -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.semantics.semantics -import androidx.compose.foundation.layout.navigationBarsPadding +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.onboarding.model.OnboardingSecureAppMethod import de.gematik.ti.erp.app.onboarding.ui.OnboardingBottomBar import de.gematik.ti.erp.app.theme.AppTheme +import de.gematik.ti.erp.app.theme.PaddingDefaults +import de.gematik.ti.erp.app.userauthentication.ui.BiometricPrompt import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold +import de.gematik.ti.erp.app.utils.compose.NavigationBarMode @Composable fun AllowBiometryScreen( diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt index 0c96447b..3d944f5f 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/AuditEventsScreen.kt @@ -38,12 +38,12 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemsIndexed -import de.gematik.ti.erp.app.R +import androidx.paging.compose.itemKey import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.mainscreen.presentation.rememberMainScreenController import de.gematik.ti.erp.app.mainscreen.ui.RefreshScaffold -import de.gematik.ti.erp.app.mainscreen.ui.rememberMainScreenController import de.gematik.ti.erp.app.prescription.ui.rememberRefreshPrescriptionsController import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.settings.AuditEventsController @@ -73,7 +73,7 @@ fun AuditEventsScreen( onBack: () -> Unit ) { val header = stringResource(R.string.autitEvents_headline) - val pagingItems = auditEventsController.auditEventPagingFlow.collectAsLazyPagingItems() + val auditItems = auditEventsController.auditEventPagingFlow.collectAsLazyPagingItems() val listState = rememberLazyListState() val mainScreenController = rememberMainScreenController() @@ -115,7 +115,7 @@ fun AuditEventsScreen( mainScreenController = mainScreenController, onShowCardWall = onShowCardWall ) { - if (pagingItems.itemCount == 0) { + if (auditItems.itemCount == 0) { LazyColumn( modifier = Modifier .padding(innerPadding) @@ -151,7 +151,11 @@ fun AuditEventsScreen( state = listState, contentPadding = WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues() ) { - itemsIndexed(pagingItems) { _, auditEvent -> + items( + count = auditItems.itemCount, + key = auditItems.itemKey { it.auditId } + ) { index -> + val auditEvent = auditItems[index] auditEvent?.let { Column( modifier = Modifier.padding(PaddingDefaults.Medium) @@ -164,7 +168,6 @@ fun AuditEventsScreen( .toLocalDateTime(TimeZone.currentSystemDefault()) .toJavaLocalDateTime() } - Text( phrasedDateString(date = timestamp), style = AppTheme.typography.body2l diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt index c57bcb19..3d28aff9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/DeviceSecuritySettingsScreen.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt index a6b3070c..9ff4019c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PasswordScreen.kt @@ -78,8 +78,8 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.nulabinc.zxcvbn.Zxcvbn -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt index c5f901b8..9b26201c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/PharmacyLicenseScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.provideLinkForString import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt index 72f56290..3440e5ef 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/ProductImprovementSettingsScreen.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.semantics -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt index 27adbb96..cd391ee6 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsController.kt @@ -22,8 +22,8 @@ import android.accessibilityservice.AccessibilityServiceInfo import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.analytics.Analytics import de.gematik.ti.erp.app.settings.model.SettingsData @@ -44,7 +44,7 @@ class SettingsController( val analyticsState @Composable - get() = analyticsFlow.collectAsState(SettingStatesData.defaultAnalyticsState) + get() = analyticsFlow.collectAsStateWithLifecycle(SettingStatesData.defaultAnalyticsState) private val authenticationModeFlow = settingsUseCase.authenticationMode.map { SettingStatesData.AuthenticationModeState( @@ -54,13 +54,13 @@ class SettingsController( val authenticationModeState @Composable - get() = authenticationModeFlow.collectAsState(SettingStatesData.defaultAuthenticationState) + get() = authenticationModeFlow.collectAsStateWithLifecycle(SettingStatesData.defaultAuthenticationState) private val zoomFlow = settingsUseCase.general.map { SettingStatesData.ZoomState(it.zoomEnabled) } val zoomState @Composable - get() = zoomFlow.collectAsState(SettingStatesData.defaultZoomState) + get() = zoomFlow.collectAsStateWithLifecycle(SettingStatesData.defaultZoomState) private val screenShotFlow = settingsUseCase.general.map { SettingStatesData.ScreenshotState(it.screenShotsAllowed) @@ -68,7 +68,7 @@ class SettingsController( val screenshotState @Composable - get() = screenShotFlow.collectAsState(SettingStatesData.defaultScreenshotState) + get() = screenShotFlow.collectAsStateWithLifecycle(SettingStatesData.defaultScreenshotState) suspend fun onSelectDeviceSecurityAuthenticationMode() { settingsUseCase.saveAuthenticationMode( diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt similarity index 86% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt index df065ed8..c6b5d753 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsNavigation.kt @@ -27,10 +27,15 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.MainActivity import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.Route -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.demomode.DemoModeIntent +import de.gematik.ti.erp.app.demomode.startAppWithDemoMode +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens 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 @@ -50,7 +55,8 @@ fun SettingsNavGraph( settingsNavController: NavHostController, navigationMode: NavigationMode, mainNavController: NavController, - settingsController: SettingsController + settingsController: SettingsController, + buildConfig: BuildConfigInformation ) { val scope = rememberCoroutineScope() NavHost( @@ -58,10 +64,15 @@ fun SettingsNavGraph( startDestination = SettingsNavigationScreens.Settings.path() ) { composable(SettingsNavigationScreens.Settings.route) { + val activity = LocalActivity.current NavigationAnimation(mode = navigationMode) { SettingsScreenWithScaffold( mainNavController = mainNavController, - navController = settingsNavController + navController = settingsNavController, + buildConfig = buildConfig, + onClickDemoMode = { + DemoModeIntent.startAppWithDemoMode(activity) + } ) } } @@ -107,6 +118,7 @@ fun SettingsNavGraph( when (it) { is SettingsData.AuthenticationMode.Password -> mainNavController.navigate(MainNavigationScreens.Password.path()) + else -> scope.launch { settingsController.onSelectDeviceSecurityAuthenticationMode() diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt similarity index 88% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt index 7d64624d..94c327da 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/SettingsScreen.kt @@ -16,13 +16,14 @@ * */ +@file:Suppress("MaximumLineLength") + package de.gematik.ti.erp.app.settings.ui import android.content.Context import android.os.Build import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -84,20 +85,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.BuildConfig import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChanges import de.gematik.ti.erp.app.card.model.command.UnlockMethod -import de.gematik.ti.erp.app.cardwall.usecase.deviceHasNFC -import de.gematik.ti.erp.app.mainscreen.ui.MainNavigationScreens +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation +import de.gematik.ti.erp.app.mainscreen.navigation.MainNavigationScreens +import de.gematik.ti.erp.app.profiles.presentation.ProfilesController +import de.gematik.ti.erp.app.profiles.presentation.rememberProfilesController import de.gematik.ti.erp.app.profiles.ui.Avatar -import de.gematik.ti.erp.app.profiles.ui.ProfilesController -import de.gematik.ti.erp.app.profiles.ui.ProfilesStateData -import de.gematik.ti.erp.app.profiles.ui.rememberProfilesController -import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData.Profile.Companion.containsProfileWithName import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AlertDialog @@ -110,7 +111,8 @@ import de.gematik.ti.erp.app.utils.compose.handleIntent import de.gematik.ti.erp.app.utils.compose.navigationModeState import de.gematik.ti.erp.app.utils.compose.provideEmailIntent import de.gematik.ti.erp.app.utils.compose.providePhoneIntent -import de.gematik.ti.erp.app.utils.sanitizeProfileName +import de.gematik.ti.erp.app.utils.extensions.sanitizeProfileName +import org.kodein.di.compose.rememberInstance import java.util.Locale @Composable @@ -118,26 +120,31 @@ fun SettingsScreen( mainNavController: NavController, settingsController: SettingsController ) { + val buildConfig by rememberInstance() val settingsNavController = rememberNavController() var previousNavEntry by remember { mutableStateOf("settings") } - TrackNavigationChanges(settingsNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(settingsNavController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) val navigationMode by settingsNavController.navigationModeState(SettingsNavigationScreens.Settings.route) SettingsNavGraph( settingsNavController = settingsNavController, navigationMode = navigationMode, mainNavController = mainNavController, - settingsController = settingsController + settingsController = settingsController, + buildConfig = buildConfig ) } @Composable fun SettingsScreenWithScaffold( mainNavController: NavController, - navController: NavController + navController: NavController, + buildConfig: BuildConfigInformation, + onClickDemoMode: () -> Unit ) { + val context = LocalContext.current val profilesController = rememberProfilesController() - val profilesState by profilesController.profilesState + val profilesState by profilesController.getProfilesState() val listState = rememberLazyListState() @@ -146,7 +153,6 @@ fun SettingsScreenWithScaffold( .testTag(TestTag.Settings.SettingsScreen) .statusBarsPadding() ) { contentPadding -> - LazyColumn( modifier = Modifier.testTag("settings_screen"), contentPadding = contentPadding, @@ -157,10 +163,9 @@ fun SettingsScreenWithScaffold( "O.Source_9", "O.Source_11", sourceSpecification = "BSI-eRp-ePA", - rationale = "Debug options are not accessible in the production version." + - "All other debug mechanisms, including loogging, are disabled in the build pipeline." + rationale = "Debug options are not accessible in the production version. All other debug mechanisms, including logging, are disabled in the build pipeline." // ktlint-disable max-line-length ) - if (BuildKonfig.INTERNAL) { + if (BuildKonfig.INTERNAL && BuildConfig.DEBUG) { item { DebugMenuSection(mainNavController) } @@ -194,19 +199,29 @@ fun SettingsScreenWithScaffold( }, onClickDeviceSecuritySettings = { navController.navigate(SettingsNavigationScreens.DeviceSecuritySettings.path()) - } + }, + onClickDemoMode = onClickDemoMode ) SettingsDivider() } item { - ContactSection() + ContactSection( + darkMode = buildConfig.inDarkTheme(), + language = buildConfig.language(), + versionName = buildConfig.versionName(), + nfcInfo = buildConfig.nfcInformation(context), + phoneModel = buildConfig.model() + ) SettingsDivider() } item { LegalSection(mainNavController) } item { - AboutSection(Modifier.padding(top = 76.dp)) + AboutSection( + modifier = Modifier.padding(top = 76.dp), + buildVersionName = buildConfig.versionName() + ) } } } @@ -216,7 +231,8 @@ fun SettingsScreenWithScaffold( fun GlobalSettingsSection( onClickAccessibilitySettings: () -> Unit, onClickProductImprovementSettings: () -> Unit, - onClickDeviceSecuritySettings: () -> Unit + onClickDeviceSecuritySettings: () -> Unit, + onClickDemoMode: () -> Unit ) { Column { @@ -230,6 +246,12 @@ fun GlobalSettingsSection( top = PaddingDefaults.Medium ) ) + LabelButton( + icon = painterResource(R.drawable.magic_wand_filled), + stringResource(R.string.demo_mode_settings_title) + ) { + onClickDemoMode() + } LabelButton( Icons.Outlined.AccessibilityNew, stringResource(R.string.settings_accessibility_header) @@ -261,11 +283,9 @@ private fun SettingsDivider() = @Composable private fun ProfileSection( - profilesState: ProfilesStateData.ProfilesState, + profiles: List, navController: NavController ) { - val profiles = profilesState.profiles - Column { Text( text = stringResource(R.string.settings_profiles_headline), @@ -292,7 +312,7 @@ private fun ProfileSection( @Composable private fun ProfileCard( - profile: ProfilesUseCaseData.Profile, + profile: Profile, onClickEdit: () -> Unit ) { Row( @@ -307,7 +327,7 @@ private fun ProfileCard( verticalAlignment = Alignment.CenterVertically ) { Avatar( - avatarModifier = Modifier.size(48.dp), + modifier = Modifier.size(48.dp), emptyIcon = Icons.Rounded.PersonOutline, profile = profile, ssoStatusColor = null, @@ -332,7 +352,7 @@ fun ProfileNameDialog( onEdit: (text: String) -> Unit, onDismissRequest: () -> Unit ) { - val profilesState by profilesController.profilesState + val profiles by profilesController.getProfilesState() var textValue by remember { mutableStateOf(initialProfileName) } var duplicated by remember { mutableStateOf(false) } @@ -372,7 +392,7 @@ fun ProfileNameDialog( value = textValue, singleLine = true, onValueChange = { - val isExistingProfileName = profilesState.containsProfileWithName(textValue) + val isExistingProfileName = profiles.containsProfileWithName(textValue) val isNotInitialProfileName = textValue.trim() != initialProfileName textValue = it.trimStart().sanitizeProfileName() duplicated = isNotInitialProfileName && isExistingProfileName && !wantRemoveLastProfile @@ -601,7 +621,10 @@ private fun LabelButton( } @Composable -private fun AboutSection(modifier: Modifier) { +private fun AboutSection( + modifier: Modifier, + buildVersionName: String +) { Column( modifier = modifier .padding(PaddingDefaults.Medium) @@ -616,7 +639,7 @@ private fun AboutSection(modifier: Modifier) { Icon(Icons.Rounded.PhoneAndroid, null, modifier = Modifier.size(16.dp)) SpacerTiny() Text( - stringResource(R.string.about_version, BuildConfig.VERSION_NAME) + stringResource(R.string.about_version, buildVersionName) ) } SpacerTiny() @@ -628,7 +651,13 @@ private fun AboutSection(modifier: Modifier) { } @Composable -private fun ContactSection() { +private fun ContactSection( + darkMode: String, + versionName: String, + language: String, + phoneModel: String, + nfcInfo: String +) { val context = LocalContext.current val contactHeader = stringResource(R.string.settings_contact_headline) @@ -636,8 +665,13 @@ private fun ContactSection() { val phoneNumber = stringResource(R.string.settings_contact_hotline_number) val mailAddress = stringResource(R.string.settings_contact_mail_address) val subject = stringResource(R.string.settings_feedback_mail_subject) - val body = buildFeedbackBodyWithDeviceInfo(context = context) - + val body = buildFeedbackBodyWithDeviceInfo( + darkMode = darkMode, + versionName = versionName, + language = language, + phoneModel = phoneModel, + nfcInfo = nfcInfo + ) SpacerMedium() Text( text = contactHeader, @@ -645,7 +679,6 @@ private fun ContactSection() { modifier = Modifier.padding(horizontal = PaddingDefaults.Medium) ) SpacerSmall() - LabelButton( icon = Icons.Outlined.Mail, text = stringResource(R.string.settings_contact_feedback_form), @@ -682,11 +715,14 @@ fun openMailClient( @Suppress("MaxLineLength") @Composable fun buildFeedbackBodyWithDeviceInfo( - context: Context, title: String = stringResource(R.string.settings_feedback_mail_title), userHint: String = stringResource(R.string.seetings_feedback_form_additional_data_info), errorState: String? = null, - darkMode: Boolean = isSystemInDarkTheme() + darkMode: String, + versionName: String, + language: String, + phoneModel: String, + nfcInfo: String ): String = """$title | | @@ -696,11 +732,11 @@ fun buildFeedbackBodyWithDeviceInfo( |Systeminformationen | |Betriebssystem: Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) (PATCH ${Build.VERSION.SECURITY_PATCH}) - |Modell: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.PRODUCT}) - |App Version: ${BuildConfig.VERSION_NAME} (${BuildKonfig.GIT_HASH}) - |DarkMode: ${if (darkMode) "an" else "aus"} - |Sprache: ${Locale.getDefault().displayName} + |Modell: $phoneModel + |App Version: $versionName (${BuildKonfig.GIT_HASH}) + |DarkMode: $darkMode + |Sprache: $language |FehlerStatus: ${errorState ?: ""} - |NFC: ${if (context.deviceHasNFC()) "vorhanden" else "nicht vorhanden"} + |NFC: $nfcInfo | """.trimMargin() diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt index 0278b892..9ebdf91c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/ui/TokenScreen.kt @@ -46,13 +46,13 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.TestTag.Profile.TokenList.AccessToken import de.gematik.ti.erp.app.TestTag.Profile.TokenList.NoTokenHeader import de.gematik.ti.erp.app.TestTag.Profile.TokenList.NoTokenInfo import de.gematik.ti.erp.app.TestTag.Profile.TokenList.SSOToken +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import de.gematik.ti.erp.app.utils.compose.AnimatedElevationScaffold diff --git a/android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/settings/usecase/SettingsUseCase.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/theme/Color.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/theme/Color.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Color.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/theme/Shape.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/theme/Shape.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Shape.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/theme/Theme.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/theme/Theme.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt index 0fce01c3..7b080317 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/theme/Theme.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Theme.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.em -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R @Suppress("LongMethod") @Composable diff --git a/android/src/main/java/de/gematik/ti/erp/app/theme/Type.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/theme/Type.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/theme/Type.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt index 73aac863..c4412049 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/troubleShooting/TroubleshootingContent.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/troubleshooting/TroubleshootingContent.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.troubleShooting +package de.gematik.ti.erp.app.troubleshooting import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -24,9 +24,13 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -36,6 +40,10 @@ import androidx.compose.material.icons.outlined.Lightbulb import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.Edit 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 @@ -45,20 +53,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.Route -import de.gematik.ti.erp.app.analytics.TrackNavigationChanges +import de.gematik.ti.erp.app.analytics.trackNavigationChanges +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.info.BuildConfigInformation import de.gematik.ti.erp.app.settings.ui.buildFeedbackBodyWithDeviceInfo import de.gematik.ti.erp.app.settings.ui.openMailClient import de.gematik.ti.erp.app.theme.AppTheme @@ -75,6 +76,7 @@ import de.gematik.ti.erp.app.utils.compose.SpacerSmall import de.gematik.ti.erp.app.utils.compose.SpacerTiny import de.gematik.ti.erp.app.utils.compose.annotatedLinkString import de.gematik.ti.erp.app.utils.compose.annotatedStringResource +import org.kodein.di.compose.rememberInstance object TroubleShootingNavigation { object TroubleshootingPageA : Route("troubleShooting") @@ -89,8 +91,9 @@ fun TroubleShootingScreen( onCancel: () -> Unit ) { val navController = rememberNavController() + val buildConfig by rememberInstance() var previousNavEntry by remember { mutableStateOf("troubleShooting") } - TrackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) + trackNavigationChanges(navController, previousNavEntry, onNavEntryChange = { previousNavEntry = it }) NavHost( navController, startDestination = TroubleShootingNavigation.TroubleshootingPageA.path() @@ -125,6 +128,7 @@ fun TroubleShootingScreen( composable(TroubleShootingNavigation.TroubleshootingNoSuccessPage.route) { NavigationAnimation { TroubleshootingNoSuccessPageContent( + buildConfig = buildConfig, onNext = onCancel, onBack = { navController.popBackStack() } ) @@ -232,6 +236,7 @@ fun TroubleshootingPageCContent( @Composable fun TroubleshootingNoSuccessPageContent( + buildConfig: BuildConfigInformation, onNext: () -> Unit, onBack: () -> Unit ) { @@ -243,7 +248,13 @@ fun TroubleshootingNoSuccessPageContent( val context = LocalContext.current val mailAddress = stringResource(R.string.settings_contact_mail_address) val subject = stringResource(R.string.settings_feedback_mail_subject) - val body = buildFeedbackBodyWithDeviceInfo(context = context) + val body = buildFeedbackBodyWithDeviceInfo( + darkMode = buildConfig.inDarkTheme(), + language = buildConfig.language(), + versionName = buildConfig.versionName(), + nfcInfo = buildConfig.nfcInformation(context), + phoneModel = buildConfig.model() + ) Column { Text( diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt index ced98b06..caf285f9 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/AuthenticationUseCase.kt @@ -25,13 +25,14 @@ import androidx.lifecycle.Lifecycle.Event.ON_START import androidx.lifecycle.Lifecycle.Event.ON_STOP import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.Requirement import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.model.SettingsData.AuthenticationMode.Password import de.gematik.ti.erp.app.settings.usecase.SettingsUseCase import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.delay @@ -71,13 +72,13 @@ private const val ResetTimeout = -1L ) class AuthenticationUseCase( private val settingsUseCase: SettingsUseCase, - dispatchers: DispatchProvider + dispatcher: CoroutineDispatcher = Dispatchers.IO ) : LifecycleEventObserver { private enum class Lifecycle { Created, Started, Running, Paused } - private val scope = CoroutineScope(dispatchers.Default) + private val scope = CoroutineScope(dispatcher) private val authRequired = MutableStateFlow(false) private val lifecycle = MutableStateFlow(Lifecycle.Created) @@ -159,7 +160,7 @@ class AuthenticationUseCase( send(it) } - }.flowOn(dispatchers.Default) + }.flowOn(dispatcher) .shareIn(scope = scope, started = SharingStarted.Lazily, replay = 1) // end::AuthenticationUseCase[] diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/BiometricPrompt.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt similarity index 93% rename from android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt index d173e4fc..296298fc 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationController.kt @@ -20,8 +20,8 @@ package de.gematik.ti.erp.app.userauthentication.ui import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.gematik.ti.erp.app.settings.model.SettingsData import kotlinx.coroutines.flow.map import org.kodein.di.compose.rememberInstance @@ -46,7 +46,7 @@ class AuthenticationController( val authenticationState @Composable - get() = authenticationFlow.collectAsState(AuthenticationStateData.defaultAuthenticationState) + get() = authenticationFlow.collectAsStateWithLifecycle(AuthenticationStateData.defaultAuthenticationState) suspend fun isPasswordValid(password: String): Boolean = authUseCase.isPasswordValid(password) diff --git a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt index 3cc86fe6..5f1872ee 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/userauthentication/ui/UserAuthenticationScreenComponents.kt @@ -31,16 +31,21 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.LockOpen import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,13 +64,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.Scaffold -import androidx.compose.runtime.LaunchedEffect import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.BuildConfig +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.settings.model.SettingsData import de.gematik.ti.erp.app.settings.ui.PasswordTextField import de.gematik.ti.erp.app.theme.AppTheme @@ -379,7 +380,7 @@ private fun PasswordPrompt( AlertDialog( onDismissRequest = onCancel, buttons = { - if (BuildKonfig.INTERNAL) { + if (BuildKonfig.INTERNAL && BuildConfig.DEBUG) { OutlinedDebugButton("SKIP", onClick = onAuthenticated) } TextButton( diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/AnimatedElevationScaffold.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Animations.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt similarity index 76% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Animations.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt index 14761d96..dcbbde71 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Animations.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Animations.kt @@ -27,6 +27,7 @@ import androidx.compose.animation.slideInVertically import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember @@ -34,7 +35,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.navigation.NavHostController -import kotlinx.coroutines.flow.collect enum class NavigationMode { Forward, @@ -64,32 +64,36 @@ fun NavigationAnimation( ) } +// TODO: Do navigation mode without maintaining your own state +@Deprecated( + message = "This is completely wrong way to do navigation mode.", + replaceWith = ReplaceWith("another way of doing navigation mode") +) @Composable fun NavHostController.navigationModeState( startDestination: String, intercept: ((previousRoute: String?, currentRoute: String?) -> NavigationMode?)? = null ): State { - var prevNumOfEntries by rememberSaveable(this, startDestination) { mutableStateOf(-1) } + var prevNumOfEntries by rememberSaveable(this, startDestination) { mutableIntStateOf(-1) } var prevRoute by rememberSaveable(this, startDestination) { mutableStateOf(null) } return produceState(intercept?.invoke(null, startDestination) ?: NavigationMode.Open) { this@navigationModeState.currentBackStackEntryFlow.collect { - val currRoute = it.destination.route - val interceptedMode = if (intercept != null) { - intercept(prevRoute, currRoute) - } else { - null + val currentRoute = it.destination.route + val interceptedMode = when { + intercept != null -> intercept(prevRoute, currentRoute) + else -> null } value = interceptedMode ?: when { - prevNumOfEntries == -1 && currRoute == startDestination -> NavigationMode.Open - this@navigationModeState.backQueue.size < prevNumOfEntries -> NavigationMode.Back - this@navigationModeState.backQueue.size > prevNumOfEntries -> NavigationMode.Forward + prevNumOfEntries == -1 && currentRoute == startDestination -> NavigationMode.Open + this@navigationModeState.currentBackStack.value.size < prevNumOfEntries -> NavigationMode.Back + this@navigationModeState.currentBackStack.value.size > prevNumOfEntries -> NavigationMode.Forward else -> NavigationMode.Open } - prevRoute = currRoute - prevNumOfEntries = this@navigationModeState.backQueue.size + prevRoute = currentRoute + prevNumOfEntries = this@navigationModeState.currentBackStack.value.size } } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomBars.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt index e41fe8d0..9e2af4af 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomBars.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomBars.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheet.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/BottomSheetAction.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Buttons.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Buttons.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt index 3e5dd3eb..99716dab 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Buttons.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Buttons.kt @@ -36,7 +36,6 @@ import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults // TODO replace with material3 - @Composable fun SecondaryButton( onClick: () -> Unit, diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Chip.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Chip.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Chip.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt new file mode 100644 index 00000000..e4899d16 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ClickableText.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.annotation.StringRes +import androidx.compose.foundation.text.ClickableText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import de.gematik.ti.erp.app.theme.AppTheme + +/** + * A clickable text which allows you to give in the text string and the link-text + * and it automatically builds the clickable listener on the text + */ +@Composable +fun ClickableText( + modifier: Modifier = Modifier, + @StringRes textWithPlaceholdersRes: Int, + clickText: ClickText, + textStyle: TextStyle, + linkTextStyle: SpanStyle = SpanStyle(color = AppTheme.colors.primary600), + text: String = stringResource(id = textWithPlaceholdersRes) +) { + data class TextData( + val text: String, + val tag: String? = null, + val onClick: (() -> Unit)? = null + ) + + val resources = LocalContext.current.resources + val textData = remember(clickText, text, resources) { + val regex = Regex(clickText.text) + val splits = text.splitToSequence(regex).toMutableList() + // add the delimiter also to the list + splits.add(1, clickText.text) + mutableListOf().apply { + splits.forEach { part -> + if (part == clickText.text) { + add( + TextData( + text = clickText.text, + tag = "$clickText-link-text-tag", + onClick = { clickText.onClick() } + ) + ) + } else { + add(TextData(text = part)) + } + } + } + } + val annotatedString = remember(textData) { + buildAnnotatedString { + textData.forEach { data -> + if (data.tag != null) { + pushStringAnnotation( + tag = data.tag, + annotation = "link-text" + ) + withStyle(style = linkTextStyle) { + append(data.text) + } + pop() + } else { + withStyle(style = textStyle.toSpanStyle()) { + append(data.text) + } + } + } + } + } + ClickableText( + modifier = modifier, + text = annotatedString, + style = textStyle, + onClick = { offset -> + textData.forEach { annotatedStringData -> + if (annotatedStringData.tag != null) { + annotatedString.getStringAnnotations( + tag = annotatedStringData.tag, + start = offset, + end = offset + ).firstOrNull()?.let { + annotatedStringData.onClick?.invoke() + } + } + } + } + ) +} + +data class ClickText( + val text: String, + val onClick: () -> Unit +) diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt index 01bf3102..8a52fb07 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Common.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Common.kt @@ -119,9 +119,9 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import de.gematik.ti.erp.app.BuildKonfig -import de.gematik.ti.erp.app.R import de.gematik.ti.erp.app.TestTag import de.gematik.ti.erp.app.core.LocalActivity +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import io.github.aakira.napier.Napier diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/ComposableEvent.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/DMCode.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/DMCode.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/DataMatrix.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Dialog.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Dialog.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Dialog.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt new file mode 100644 index 00000000..1b7b1555 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/EditableHeaderText.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import de.gematik.ti.erp.app.features.R +import de.gematik.ti.erp.app.theme.AppTheme + +const val TextMinLength = 1 +const val MaxLines = 3 + +@Composable +fun EditableHeaderTextField( + text: String, + textMinLength: Int = TextMinLength, + onSaveText: (String) -> Unit +) { + var isEditing by remember { mutableStateOf(false) } + + var name by remember { + mutableStateOf(text) + } + + if (isEditing) { + EditableTextField(text, textMinLength) { + onSaveText(it) + isEditing = false + name = it + } + } else { + HeaderText(name) { + isEditing = true + } + } +} + +@Composable +private fun HeaderText(name: String, onClickEdit: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth().wrapContentWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(WEIGHT_1_0).wrapContentWidth(), + text = name, + style = AppTheme.typography.h5, + textAlign = TextAlign.Center, + maxLines = MaxLines, + overflow = TextOverflow.Ellipsis + ) + EditIconButton { onClickEdit() } + } +} + +@Composable +private fun EditIconButton( + onClick: () -> Unit +) { + IconButton( + onClick = onClick + ) { + Icon(Icons.Outlined.Edit, null, tint = AppTheme.colors.neutral600) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun EditableTextField( + text: String, + textMinLength: Int, + onSaveText: (String) -> Unit +) { + var name by remember { + mutableStateOf(text) + } + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + var isError by remember { mutableStateOf(false) } + + val keyboardOption = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ) + + val keyboardActionsDone = KeyboardActions( + onDone = { + if (!isError) { + focusManager.clearFocus() + keyboardController?.hide() + onSaveText(name.trim()) + } + } + ) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + modifier = Modifier.wrapContentWidth().focusRequester(focusRequester), + colors = basicTextFieldColors, + textStyle = AppTheme.typography.h5.copy(textAlign = TextAlign.Center), + keyboardOptions = keyboardOption, + isError = isError, + label = { + if (isError) { + Text(stringResource(R.string.empty_scanned_prescription_name)) + } + }, + value = name, + keyboardActions = keyboardActionsDone, + onValueChange = { + name = it.trimStart() + isError = name.length < textMinLength + } + ) + } +} + +private val basicTextFieldColors + @Composable + get() = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Unspecified, + unfocusedIndicatorColor = Color.Unspecified + ) diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt similarity index 99% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt index 64356973..c6297804 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Hints.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Hints.kt @@ -81,7 +81,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import de.gematik.ti.erp.app.theme.AppTheme import de.gematik.ti.erp.app.theme.PaddingDefaults import kotlinx.coroutines.delay diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/InsetAwareBars.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/LightDarkPreview.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Spacer.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Spacer.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Spacer.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Spacer.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt index e748dba4..eeb36968 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/TimeDescription.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource -import de.gematik.ti.erp.app.R +import de.gematik.ti.erp.app.features.R import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/Title.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Title.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/Title.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Title.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/compose/ComposeToastShort.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/utils/compose/ComposeToastShort.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toast.kt diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toasts.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toasts.kt new file mode 100644 index 00000000..759bd524 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Toasts.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +import android.content.Context +import android.widget.Toast + +fun createToastShort(context: Context, text: String) = Toast.makeText(context, text, Toast.LENGTH_SHORT).show() diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt new file mode 100644 index 00000000..48c55561 --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/compose/Weigts.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.compose + +const val WEIGHT_0_7 = 0.7f +const val WEIGHT_0_5 = 0.5f +const val WEIGHT_0_3 = 0.3f +const val WEIGHT_1_0 = 1.0f diff --git a/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt new file mode 100644 index 00000000..05bf2dec --- /dev/null +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ContextExtensions.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.utils.extensions + +import android.content.Context +import android.os.Build +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability + +fun Context.isGooglePlayServiceAvailable(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt index 7f290481..6d2dccbe 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/DateTime.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/DateTime.kt @@ -16,11 +16,12 @@ * */ -package de.gematik.ti.erp.app.utils +package de.gematik.ti.erp.app.utils.extensions import android.os.Build import de.gematik.ti.erp.app.fhir.parser.toJavaYear import de.gematik.ti.erp.app.fhir.parser.toJavaYearMonth +import de.gematik.ti.erp.app.utils.FhirTemporal import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDate diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/Brightness.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt similarity index 89% rename from android/src/main/java/de/gematik/ti/erp/app/utils/Brightness.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt index 8c0eb931..9c5a2dfa 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/Brightness.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/ForceBrightness.kt @@ -16,15 +16,17 @@ * */ -package de.gematik.ti.erp.app.utils +package de.gematik.ti.erp.app.utils.extensions +import android.annotation.SuppressLint import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +@SuppressLint("ComposableNaming") @Composable -fun ComponentActivity.ForceBrightness() { +fun ComponentActivity.forceBrightness() { DisposableEffect(Unit) { val attributes = window?.attributes val originalBrightness = attributes?.screenBrightness diff --git a/android/src/main/java/de/gematik/ti/erp/app/utils/TextUtil.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/utils/TextUtil.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt index c4460c84..d7721e5c 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/utils/TextUtil.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/utils/extensions/TextUtil.kt @@ -16,7 +16,7 @@ * */ -package de.gematik.ti.erp.app.utils +package de.gematik.ti.erp.app.utils.extensions import kotlin.streams.asSequence @@ -28,6 +28,7 @@ private val r = """[\p{L}\p{M}\p{N}\u200d -]""".toRegex() /** * Take the String and map the characters that are characters or emoticon and return the string back */ +// TODO: Check if we need this fixed fun String.sanitizeProfileName() = codePoints() .asSequence() diff --git a/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt similarity index 97% rename from android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt index 38bea7b3..464e8d32 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/vau/VauModule.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/VauModule.kt @@ -19,7 +19,6 @@ package de.gematik.ti.erp.app.vau import de.gematik.ti.erp.app.di.EndpointHelper -import de.gematik.ti.erp.app.di.NetworkSecurePreferencesTag 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 @@ -40,6 +39,8 @@ import org.kodein.di.bindSingleton import org.kodein.di.instance import kotlin.time.Duration +const val NetworkSecurePreferencesTag = "NetworkSecurePreferences" + val vauModule = DI.Module("vauModule") { bindSingleton { val endpointHelper = instance() diff --git a/android/src/main/java/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt index be23fe39..3e5d42f3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt +++ b/app/features/src/main/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt @@ -82,7 +82,7 @@ class VauChannelInterceptor( } try { - val encryptedRequest = runBlocking(dispatchers.IO) { + val encryptedRequest = runBlocking(dispatchers.io) { truststore.withValidVauPublicKey { publicKey -> VauChannelSpec.V1.encryptHttpRequest( chain.request(), diff --git a/android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt b/app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/webview/WebViewScreen.kt rename to app/features/src/main/kotlin/de/gematik/ti/erp/app/webview/WebViewScreen.kt diff --git a/android/src/main/res/drawable-xhdpi/alarm_clock.webp b/app/features/src/main/res/drawable-xhdpi/alarm_clock.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/alarm_clock.webp rename to app/features/src/main/res/drawable-xhdpi/alarm_clock.webp diff --git a/android/src/main/res/drawable-xhdpi/baby_portrait.webp b/app/features/src/main/res/drawable-xhdpi/baby_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/baby_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/baby_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/baby_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/baby_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/baby_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/baby_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/boy_with_health_card_portrait.webp b/app/features/src/main/res/drawable-xhdpi/boy_with_health_card_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/boy_with_health_card_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/boy_with_health_card_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/boy_with_health_card_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/boy_with_health_card_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/boy_with_health_card_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/boy_with_health_card_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/card_wall_card_can.webp b/app/features/src/main/res/drawable-xhdpi/card_wall_card_can.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/card_wall_card_can.webp rename to app/features/src/main/res/drawable-xhdpi/card_wall_card_can.webp diff --git a/android/src/main/res/drawable-xhdpi/card_wall_card_hand.webp b/app/features/src/main/res/drawable-xhdpi/card_wall_card_hand.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/card_wall_card_hand.webp rename to app/features/src/main/res/drawable-xhdpi/card_wall_card_hand.webp diff --git a/android/src/main/res/drawable-xhdpi/clapping_hands_blue.webp b/app/features/src/main/res/drawable-xhdpi/clapping_hands_blue.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/clapping_hands_blue.webp rename to app/features/src/main/res/drawable-xhdpi/clapping_hands_blue.webp diff --git a/android/src/main/res/drawable-xhdpi/crew.webp b/app/features/src/main/res/drawable-xhdpi/crew.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/crew.webp rename to app/features/src/main/res/drawable-xhdpi/crew.webp diff --git a/android/src/main/res/drawable-xhdpi/delivery_car_small.webp b/app/features/src/main/res/drawable-xhdpi/delivery_car_small.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/delivery_car_small.webp rename to app/features/src/main/res/drawable-xhdpi/delivery_car_small.webp diff --git a/android/src/main/res/drawable-xhdpi/developer.webp b/app/features/src/main/res/drawable-xhdpi/developer.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/developer.webp rename to app/features/src/main/res/drawable-xhdpi/developer.webp diff --git a/android/src/main/res/drawable-xhdpi/doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xhdpi/doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/egk_on_blue_circle.webp b/app/features/src/main/res/drawable-xhdpi/egk_on_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/egk_on_blue_circle.webp rename to app/features/src/main/res/drawable-xhdpi/egk_on_blue_circle.webp diff --git a/android/src/main/res/drawable-xhdpi/erp_logo.webp b/app/features/src/main/res/drawable-xhdpi/erp_logo.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/erp_logo.webp rename to app/features/src/main/res/drawable-xhdpi/erp_logo.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_developer_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_developer_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_developer_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_developer_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_developer_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_developer_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_developer_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_developer_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_doctor_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_doctor_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_doctor_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_doctor_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_doctor_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_doctor_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_doctor_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_doctor_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/femal_doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/femal_doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/femal_doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/femal_doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/girl_red_oh_no.webp b/app/features/src/main/res/drawable-xhdpi/girl_red_oh_no.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/girl_red_oh_no.webp rename to app/features/src/main/res/drawable-xhdpi/girl_red_oh_no.webp diff --git a/android/src/main/res/drawable-xhdpi/grand_father_portrait.webp b/app/features/src/main/res/drawable-xhdpi/grand_father_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/grand_father_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/grand_father_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/grand_father_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/grand_father_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/grand_father_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/grand_father_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/grand_mother_portrait.webp b/app/features/src/main/res/drawable-xhdpi/grand_mother_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/grand_mother_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/grand_mother_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/grand_mother_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/grand_mother_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/grand_mother_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/grand_mother_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/illustration_girl.webp b/app/features/src/main/res/drawable-xhdpi/illustration_girl.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/illustration_girl.webp rename to app/features/src/main/res/drawable-xhdpi/illustration_girl.webp diff --git a/android/src/main/res/drawable-xhdpi/information.webp b/app/features/src/main/res/drawable-xhdpi/information.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/information.webp rename to app/features/src/main/res/drawable-xhdpi/information.webp diff --git a/android/src/main/res/drawable-xhdpi/laptop_woman_pink.webp b/app/features/src/main/res/drawable-xhdpi/laptop_woman_pink.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/laptop_woman_pink.webp rename to app/features/src/main/res/drawable-xhdpi/laptop_woman_pink.webp diff --git a/android/src/main/res/drawable-xhdpi/laptop_woman_yellow.webp b/app/features/src/main/res/drawable-xhdpi/laptop_woman_yellow.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/laptop_woman_yellow.webp rename to app/features/src/main/res/drawable-xhdpi/laptop_woman_yellow.webp diff --git a/app/features/src/main/res/drawable-xhdpi/magic_wand_filled.webp b/app/features/src/main/res/drawable-xhdpi/magic_wand_filled.webp new file mode 100644 index 00000000..8925759d Binary files /dev/null and b/app/features/src/main/res/drawable-xhdpi/magic_wand_filled.webp differ diff --git a/android/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_large.webp b/app/features/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_large.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_large.webp rename to app/features/src/main/res/drawable-xhdpi/main_screen_erx_icon_gray_large.webp diff --git a/android/src/main/res/drawable-xhdpi/main_screen_erx_icon_large.webp b/app/features/src/main/res/drawable-xhdpi/main_screen_erx_icon_large.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/main_screen_erx_icon_large.webp rename to app/features/src/main/res/drawable-xhdpi/main_screen_erx_icon_large.webp diff --git a/android/src/main/res/drawable-xhdpi/man_phone_blue_circle.webp b/app/features/src/main/res/drawable-xhdpi/man_phone_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/man_phone_blue_circle.webp rename to app/features/src/main/res/drawable-xhdpi/man_phone_blue_circle.webp diff --git a/android/src/main/res/drawable-xhdpi/man_with_phone_portrait.webp b/app/features/src/main/res/drawable-xhdpi/man_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/man_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/man_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/man_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/man_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/man_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/man_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/maps_marker.webp b/app/features/src/main/res/drawable-xhdpi/maps_marker.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/maps_marker.webp rename to app/features/src/main/res/drawable-xhdpi/maps_marker.webp diff --git a/android/src/main/res/drawable-xhdpi/maps_marker_grey.webp b/app/features/src/main/res/drawable-xhdpi/maps_marker_grey.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/maps_marker_grey.webp rename to app/features/src/main/res/drawable-xhdpi/maps_marker_grey.webp diff --git a/android/src/main/res/drawable-xhdpi/maps_marker_red.webp b/app/features/src/main/res/drawable-xhdpi/maps_marker_red.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/maps_marker_red.webp rename to app/features/src/main/res/drawable-xhdpi/maps_marker_red.webp diff --git a/android/src/main/res/drawable-xhdpi/medical_hand_out_circle_red.webp b/app/features/src/main/res/drawable-xhdpi/medical_hand_out_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/medical_hand_out_circle_red.webp rename to app/features/src/main/res/drawable-xhdpi/medical_hand_out_circle_red.webp diff --git a/android/src/main/res/drawable-xhdpi/oh_no_girl_hint_red.webp b/app/features/src/main/res/drawable-xhdpi/oh_no_girl_hint_red.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/oh_no_girl_hint_red.webp rename to app/features/src/main/res/drawable-xhdpi/oh_no_girl_hint_red.webp diff --git a/android/src/main/res/drawable-xhdpi/old_man_of_color_portrait.webp b/app/features/src/main/res/drawable-xhdpi/old_man_of_color_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/old_man_of_color_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/old_man_of_color_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/old_man_of_color_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/old_man_of_color_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/old_man_of_color_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/old_man_of_color_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/onboarding_boygrannygranpa.webp b/app/features/src/main/res/drawable-xhdpi/onboarding_boygrannygranpa.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/onboarding_boygrannygranpa.webp rename to app/features/src/main/res/drawable-xhdpi/onboarding_boygrannygranpa.webp diff --git a/android/src/main/res/drawable-xhdpi/order_onb_blue_handoutmedicine.webp b/app/features/src/main/res/drawable-xhdpi/order_onb_blue_handoutmedicine.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/order_onb_blue_handoutmedicine.webp rename to app/features/src/main/res/drawable-xhdpi/order_onb_blue_handoutmedicine.webp diff --git a/android/src/main/res/drawable-xhdpi/order_onb_pharmacist_blue.webp b/app/features/src/main/res/drawable-xhdpi/order_onb_pharmacist_blue.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/order_onb_pharmacist_blue.webp rename to app/features/src/main/res/drawable-xhdpi/order_onb_pharmacist_blue.webp diff --git a/android/src/main/res/drawable-xhdpi/paragraph.webp b/app/features/src/main/res/drawable-xhdpi/paragraph.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/paragraph.webp rename to app/features/src/main/res/drawable-xhdpi/paragraph.webp diff --git a/android/src/main/res/drawable-xhdpi/pharmacist.webp b/app/features/src/main/res/drawable-xhdpi/pharmacist.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/pharmacist.webp rename to app/features/src/main/res/drawable-xhdpi/pharmacist.webp diff --git a/android/src/main/res/drawable-xhdpi/pharmacist_hint.webp b/app/features/src/main/res/drawable-xhdpi/pharmacist_hint.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/pharmacist_hint.webp rename to app/features/src/main/res/drawable-xhdpi/pharmacist_hint.webp diff --git a/android/src/main/res/drawable-xhdpi/pharmacy_small.webp b/app/features/src/main/res/drawable-xhdpi/pharmacy_small.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/pharmacy_small.webp rename to app/features/src/main/res/drawable-xhdpi/pharmacy_small.webp diff --git a/android/src/main/res/drawable-xhdpi/prescription.webp b/app/features/src/main/res/drawable-xhdpi/prescription.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/prescription.webp rename to app/features/src/main/res/drawable-xhdpi/prescription.webp diff --git a/android/src/main/res/drawable-xhdpi/share_sheet.webp b/app/features/src/main/res/drawable-xhdpi/share_sheet.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/share_sheet.webp rename to app/features/src/main/res/drawable-xhdpi/share_sheet.webp diff --git a/android/src/main/res/drawable-xhdpi/truck_small.webp b/app/features/src/main/res/drawable-xhdpi/truck_small.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/truck_small.webp rename to app/features/src/main/res/drawable-xhdpi/truck_small.webp diff --git a/android/src/main/res/drawable-xhdpi/wheel_chair_user_portrait.webp b/app/features/src/main/res/drawable-xhdpi/wheel_chair_user_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/wheel_chair_user_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/wheel_chair_user_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/wheel_chair_user_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/wheel_chair_user_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/wheel_chair_user_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/wheel_chair_user_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_red_shirt_circle_blue.webp b/app/features/src/main/res/drawable-xhdpi/woman_red_shirt_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_red_shirt_circle_blue.webp rename to app/features/src/main/res/drawable-xhdpi/woman_red_shirt_circle_blue.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_red_shirt_circle_red.webp b/app/features/src/main/res/drawable-xhdpi/woman_red_shirt_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_red_shirt_circle_red.webp rename to app/features/src/main/res/drawable-xhdpi/woman_red_shirt_circle_red.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_smartphone_circle_blue.webp b/app/features/src/main/res/drawable-xhdpi/woman_smartphone_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_smartphone_circle_blue.webp rename to app/features/src/main/res/drawable-xhdpi/woman_smartphone_circle_blue.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_with_head_scarf_portrait.webp b/app/features/src/main/res/drawable-xhdpi/woman_with_head_scarf_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_with_head_scarf_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/woman_with_head_scarf_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_with_head_scarf_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/woman_with_head_scarf_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_with_head_scarf_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/woman_with_head_scarf_small_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_with_phone_portrait.webp b/app/features/src/main/res/drawable-xhdpi/woman_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/woman_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xhdpi/woman_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xhdpi/woman_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xhdpi/woman_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xhdpi/woman_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/alarm_clock.webp b/app/features/src/main/res/drawable-xxhdpi/alarm_clock.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/alarm_clock.webp rename to app/features/src/main/res/drawable-xxhdpi/alarm_clock.webp diff --git a/android/src/main/res/drawable-xxhdpi/baby_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/baby_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/baby_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/baby_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/baby_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/baby_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/baby_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/baby_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/boy_with_health_card_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/boy_with_health_card_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/boy_with_health_card_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/boy_with_health_card_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/boy_with_health_card_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/boy_with_health_card_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/boy_with_health_card_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/boy_with_health_card_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/card_wall_card_can.webp b/app/features/src/main/res/drawable-xxhdpi/card_wall_card_can.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/card_wall_card_can.webp rename to app/features/src/main/res/drawable-xxhdpi/card_wall_card_can.webp diff --git a/android/src/main/res/drawable-xxhdpi/card_wall_card_hand.webp b/app/features/src/main/res/drawable-xxhdpi/card_wall_card_hand.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/card_wall_card_hand.webp rename to app/features/src/main/res/drawable-xxhdpi/card_wall_card_hand.webp diff --git a/android/src/main/res/drawable-xxhdpi/clapping_hands_blue.webp b/app/features/src/main/res/drawable-xxhdpi/clapping_hands_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/clapping_hands_blue.webp rename to app/features/src/main/res/drawable-xxhdpi/clapping_hands_blue.webp diff --git a/android/src/main/res/drawable-xxhdpi/crew.webp b/app/features/src/main/res/drawable-xxhdpi/crew.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/crew.webp rename to app/features/src/main/res/drawable-xxhdpi/crew.webp diff --git a/android/src/main/res/drawable-xxhdpi/delivery_car_small.webp b/app/features/src/main/res/drawable-xxhdpi/delivery_car_small.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/delivery_car_small.webp rename to app/features/src/main/res/drawable-xxhdpi/delivery_car_small.webp diff --git a/android/src/main/res/drawable-xxhdpi/developer.webp b/app/features/src/main/res/drawable-xxhdpi/developer.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/developer.webp rename to app/features/src/main/res/drawable-xxhdpi/developer.webp diff --git a/android/src/main/res/drawable-xxhdpi/doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/egk_on_blue_circle.webp b/app/features/src/main/res/drawable-xxhdpi/egk_on_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/egk_on_blue_circle.webp rename to app/features/src/main/res/drawable-xxhdpi/egk_on_blue_circle.webp diff --git a/android/src/main/res/drawable-xxhdpi/erp_logo.webp b/app/features/src/main/res/drawable-xxhdpi/erp_logo.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/erp_logo.webp rename to app/features/src/main/res/drawable-xxhdpi/erp_logo.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_developer_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_developer_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_developer_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_developer_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_developer_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_developer_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_developer_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_developer_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_doctor_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_doctor_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_doctor_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_doctor_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_doctor_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_doctor_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_doctor_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_doctor_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/femal_doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/girl_red_oh_no.webp b/app/features/src/main/res/drawable-xxhdpi/girl_red_oh_no.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/girl_red_oh_no.webp rename to app/features/src/main/res/drawable-xxhdpi/girl_red_oh_no.webp diff --git a/android/src/main/res/drawable-xxhdpi/grand_father_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/grand_father_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/grand_father_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/grand_father_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/grand_father_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/grand_father_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/grand_father_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/grand_father_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/grand_mother_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/grand_mother_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/grand_mother_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/grand_mother_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/grand_mother_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/grand_mother_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/grand_mother_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/grand_mother_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/illustration_girl.webp b/app/features/src/main/res/drawable-xxhdpi/illustration_girl.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/illustration_girl.webp rename to app/features/src/main/res/drawable-xxhdpi/illustration_girl.webp diff --git a/android/src/main/res/drawable-xxhdpi/information.webp b/app/features/src/main/res/drawable-xxhdpi/information.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/information.webp rename to app/features/src/main/res/drawable-xxhdpi/information.webp diff --git a/android/src/main/res/drawable-xxhdpi/laptop_woman_pink.webp b/app/features/src/main/res/drawable-xxhdpi/laptop_woman_pink.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/laptop_woman_pink.webp rename to app/features/src/main/res/drawable-xxhdpi/laptop_woman_pink.webp diff --git a/android/src/main/res/drawable-xxhdpi/laptop_woman_yellow.webp b/app/features/src/main/res/drawable-xxhdpi/laptop_woman_yellow.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/laptop_woman_yellow.webp rename to app/features/src/main/res/drawable-xxhdpi/laptop_woman_yellow.webp diff --git a/app/features/src/main/res/drawable-xxhdpi/magic_wand_filled.webp b/app/features/src/main/res/drawable-xxhdpi/magic_wand_filled.webp new file mode 100644 index 00000000..3da7acb7 Binary files /dev/null and b/app/features/src/main/res/drawable-xxhdpi/magic_wand_filled.webp differ diff --git a/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_large.webp b/app/features/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_large.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_large.webp rename to app/features/src/main/res/drawable-xxhdpi/main_screen_erx_icon_gray_large.webp diff --git a/android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_large.webp b/app/features/src/main/res/drawable-xxhdpi/main_screen_erx_icon_large.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/main_screen_erx_icon_large.webp rename to app/features/src/main/res/drawable-xxhdpi/main_screen_erx_icon_large.webp diff --git a/android/src/main/res/drawable-xxhdpi/man_phone_blue_circle.webp b/app/features/src/main/res/drawable-xxhdpi/man_phone_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/man_phone_blue_circle.webp rename to app/features/src/main/res/drawable-xxhdpi/man_phone_blue_circle.webp diff --git a/android/src/main/res/drawable-xxhdpi/man_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/man_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/man_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/man_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/man_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/man_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/man_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/man_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/maps_marker.webp b/app/features/src/main/res/drawable-xxhdpi/maps_marker.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/maps_marker.webp rename to app/features/src/main/res/drawable-xxhdpi/maps_marker.webp diff --git a/android/src/main/res/drawable-xxhdpi/maps_marker_grey.webp b/app/features/src/main/res/drawable-xxhdpi/maps_marker_grey.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/maps_marker_grey.webp rename to app/features/src/main/res/drawable-xxhdpi/maps_marker_grey.webp diff --git a/android/src/main/res/drawable-xxhdpi/maps_marker_red.webp b/app/features/src/main/res/drawable-xxhdpi/maps_marker_red.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/maps_marker_red.webp rename to app/features/src/main/res/drawable-xxhdpi/maps_marker_red.webp diff --git a/android/src/main/res/drawable-xxhdpi/medical_hand_out_circle_red.webp b/app/features/src/main/res/drawable-xxhdpi/medical_hand_out_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/medical_hand_out_circle_red.webp rename to app/features/src/main/res/drawable-xxhdpi/medical_hand_out_circle_red.webp diff --git a/android/src/main/res/drawable-xxhdpi/oh_no_girl_hint_red.webp b/app/features/src/main/res/drawable-xxhdpi/oh_no_girl_hint_red.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/oh_no_girl_hint_red.webp rename to app/features/src/main/res/drawable-xxhdpi/oh_no_girl_hint_red.webp diff --git a/android/src/main/res/drawable-xxhdpi/old_man_of_color_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/old_man_of_color_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/old_man_of_color_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/old_man_of_color_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/old_man_of_color_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/old_man_of_color_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/old_man_of_color_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/old_man_of_color_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/onboarding_boygrannygranpa.webp b/app/features/src/main/res/drawable-xxhdpi/onboarding_boygrannygranpa.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/onboarding_boygrannygranpa.webp rename to app/features/src/main/res/drawable-xxhdpi/onboarding_boygrannygranpa.webp diff --git a/android/src/main/res/drawable-xxhdpi/order_onb_blue_handoutmedicine.webp b/app/features/src/main/res/drawable-xxhdpi/order_onb_blue_handoutmedicine.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/order_onb_blue_handoutmedicine.webp rename to app/features/src/main/res/drawable-xxhdpi/order_onb_blue_handoutmedicine.webp diff --git a/android/src/main/res/drawable-xxhdpi/order_onb_pharmacist_blue.webp b/app/features/src/main/res/drawable-xxhdpi/order_onb_pharmacist_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/order_onb_pharmacist_blue.webp rename to app/features/src/main/res/drawable-xxhdpi/order_onb_pharmacist_blue.webp diff --git a/android/src/main/res/drawable-xxhdpi/paragraph.webp b/app/features/src/main/res/drawable-xxhdpi/paragraph.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/paragraph.webp rename to app/features/src/main/res/drawable-xxhdpi/paragraph.webp diff --git a/android/src/main/res/drawable-xxhdpi/pharmacist.webp b/app/features/src/main/res/drawable-xxhdpi/pharmacist.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/pharmacist.webp rename to app/features/src/main/res/drawable-xxhdpi/pharmacist.webp diff --git a/android/src/main/res/drawable-xxhdpi/pharmacist_hint.webp b/app/features/src/main/res/drawable-xxhdpi/pharmacist_hint.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/pharmacist_hint.webp rename to app/features/src/main/res/drawable-xxhdpi/pharmacist_hint.webp diff --git a/android/src/main/res/drawable-xxhdpi/pharmacy_small.webp b/app/features/src/main/res/drawable-xxhdpi/pharmacy_small.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/pharmacy_small.webp rename to app/features/src/main/res/drawable-xxhdpi/pharmacy_small.webp diff --git a/android/src/main/res/drawable-xxhdpi/prescription.webp b/app/features/src/main/res/drawable-xxhdpi/prescription.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/prescription.webp rename to app/features/src/main/res/drawable-xxhdpi/prescription.webp diff --git a/android/src/main/res/drawable-xxhdpi/share_sheet.webp b/app/features/src/main/res/drawable-xxhdpi/share_sheet.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/share_sheet.webp rename to app/features/src/main/res/drawable-xxhdpi/share_sheet.webp diff --git a/android/src/main/res/drawable-xxhdpi/truck_small.webp b/app/features/src/main/res/drawable-xxhdpi/truck_small.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/truck_small.webp rename to app/features/src/main/res/drawable-xxhdpi/truck_small.webp diff --git a/android/src/main/res/drawable-xxhdpi/wheel_chair_user_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/wheel_chair_user_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/wheel_chair_user_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/wheel_chair_user_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/wheel_chair_user_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/wheel_chair_user_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/wheel_chair_user_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/wheel_chair_user_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_blue.webp b/app/features/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_blue.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_blue.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_red.webp b/app/features/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_red.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_red_shirt_circle_red.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_smartphone_circle_blue.webp b/app/features/src/main/res/drawable-xxhdpi/woman_smartphone_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_smartphone_circle_blue.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_smartphone_circle_blue.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_with_head_scarf_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/woman_with_head_scarf_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_with_head_scarf_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_with_head_scarf_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_with_head_scarf_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/woman_with_head_scarf_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_with_head_scarf_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_with_head_scarf_small_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/woman_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxhdpi/woman_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxhdpi/woman_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxhdpi/woman_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxhdpi/woman_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/alarm_clock.webp b/app/features/src/main/res/drawable-xxxhdpi/alarm_clock.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/alarm_clock.webp rename to app/features/src/main/res/drawable-xxxhdpi/alarm_clock.webp diff --git a/android/src/main/res/drawable-xxxhdpi/baby_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/baby_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/baby_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/baby_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/baby_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/baby_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/baby_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/baby_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/boy_with_health_card_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/boy_with_health_card_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/boy_with_health_card_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/boy_with_health_card_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/boy_with_health_card_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/boy_with_health_card_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/boy_with_health_card_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/boy_with_health_card_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/card_wall_card_can.webp b/app/features/src/main/res/drawable-xxxhdpi/card_wall_card_can.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/card_wall_card_can.webp rename to app/features/src/main/res/drawable-xxxhdpi/card_wall_card_can.webp diff --git a/android/src/main/res/drawable-xxxhdpi/card_wall_card_hand.webp b/app/features/src/main/res/drawable-xxxhdpi/card_wall_card_hand.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/card_wall_card_hand.webp rename to app/features/src/main/res/drawable-xxxhdpi/card_wall_card_hand.webp diff --git a/android/src/main/res/drawable-xxxhdpi/clapping_hands_blue.webp b/app/features/src/main/res/drawable-xxxhdpi/clapping_hands_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/clapping_hands_blue.webp rename to app/features/src/main/res/drawable-xxxhdpi/clapping_hands_blue.webp diff --git a/android/src/main/res/drawable-xxxhdpi/crew.webp b/app/features/src/main/res/drawable-xxxhdpi/crew.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/crew.webp rename to app/features/src/main/res/drawable-xxxhdpi/crew.webp diff --git a/android/src/main/res/drawable-xxxhdpi/delivery_car_small.webp b/app/features/src/main/res/drawable-xxxhdpi/delivery_car_small.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/delivery_car_small.webp rename to app/features/src/main/res/drawable-xxxhdpi/delivery_car_small.webp diff --git a/android/src/main/res/drawable-xxxhdpi/developer.webp b/app/features/src/main/res/drawable-xxxhdpi/developer.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/developer.webp rename to app/features/src/main/res/drawable-xxxhdpi/developer.webp diff --git a/android/src/main/res/drawable-xxxhdpi/doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/egk_on_blue_circle.webp b/app/features/src/main/res/drawable-xxxhdpi/egk_on_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/egk_on_blue_circle.webp rename to app/features/src/main/res/drawable-xxxhdpi/egk_on_blue_circle.webp diff --git a/android/src/main/res/drawable-xxxhdpi/erp_logo.webp b/app/features/src/main/res/drawable-xxxhdpi/erp_logo.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/erp_logo.webp rename to app/features/src/main/res/drawable-xxxhdpi/erp_logo.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_developer_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_developer_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_developer_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_developer_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_developer_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_developer_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_developer_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_developer_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_doctor_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_doctor_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_doctor_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_doctor_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_doctor_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_doctor_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_doctor_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_doctor_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/femal_doctor_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/girl_red_oh_no.webp b/app/features/src/main/res/drawable-xxxhdpi/girl_red_oh_no.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/girl_red_oh_no.webp rename to app/features/src/main/res/drawable-xxxhdpi/girl_red_oh_no.webp diff --git a/android/src/main/res/drawable-xxxhdpi/grand_father_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/grand_father_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/grand_father_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/grand_father_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/grand_father_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/grand_father_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/grand_father_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/grand_father_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/grand_mother_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/grand_mother_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/grand_mother_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/grand_mother_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/grand_mother_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/grand_mother_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/grand_mother_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/grand_mother_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/illustration_girl.webp b/app/features/src/main/res/drawable-xxxhdpi/illustration_girl.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/illustration_girl.webp rename to app/features/src/main/res/drawable-xxxhdpi/illustration_girl.webp diff --git a/android/src/main/res/drawable-xxxhdpi/information.webp b/app/features/src/main/res/drawable-xxxhdpi/information.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/information.webp rename to app/features/src/main/res/drawable-xxxhdpi/information.webp diff --git a/android/src/main/res/drawable-xxxhdpi/laptop_woman_pink.webp b/app/features/src/main/res/drawable-xxxhdpi/laptop_woman_pink.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/laptop_woman_pink.webp rename to app/features/src/main/res/drawable-xxxhdpi/laptop_woman_pink.webp diff --git a/android/src/main/res/drawable-xxxhdpi/laptop_woman_yellow.webp b/app/features/src/main/res/drawable-xxxhdpi/laptop_woman_yellow.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/laptop_woman_yellow.webp rename to app/features/src/main/res/drawable-xxxhdpi/laptop_woman_yellow.webp diff --git a/app/features/src/main/res/drawable-xxxhdpi/magic_wand_filled.webp b/app/features/src/main/res/drawable-xxxhdpi/magic_wand_filled.webp new file mode 100644 index 00000000..07e0ff13 Binary files /dev/null and b/app/features/src/main/res/drawable-xxxhdpi/magic_wand_filled.webp differ diff --git a/android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_gray_large.webp b/app/features/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_gray_large.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_gray_large.webp rename to app/features/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_gray_large.webp diff --git a/android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_large.webp b/app/features/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_large.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_large.webp rename to app/features/src/main/res/drawable-xxxhdpi/main_screen_erx_icon_large.webp diff --git a/android/src/main/res/drawable-xxxhdpi/man_phone_blue_circle.webp b/app/features/src/main/res/drawable-xxxhdpi/man_phone_blue_circle.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/man_phone_blue_circle.webp rename to app/features/src/main/res/drawable-xxxhdpi/man_phone_blue_circle.webp diff --git a/android/src/main/res/drawable-xxxhdpi/man_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/man_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/man_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/man_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/man_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/man_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/man_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/man_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/maps_marker.webp b/app/features/src/main/res/drawable-xxxhdpi/maps_marker.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/maps_marker.webp rename to app/features/src/main/res/drawable-xxxhdpi/maps_marker.webp diff --git a/android/src/main/res/drawable-xxxhdpi/maps_marker_grey.webp b/app/features/src/main/res/drawable-xxxhdpi/maps_marker_grey.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/maps_marker_grey.webp rename to app/features/src/main/res/drawable-xxxhdpi/maps_marker_grey.webp diff --git a/android/src/main/res/drawable-xxxhdpi/maps_marker_red.webp b/app/features/src/main/res/drawable-xxxhdpi/maps_marker_red.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/maps_marker_red.webp rename to app/features/src/main/res/drawable-xxxhdpi/maps_marker_red.webp diff --git a/android/src/main/res/drawable-xxxhdpi/medical_hand_out_circle_red.webp b/app/features/src/main/res/drawable-xxxhdpi/medical_hand_out_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/medical_hand_out_circle_red.webp rename to app/features/src/main/res/drawable-xxxhdpi/medical_hand_out_circle_red.webp diff --git a/android/src/main/res/drawable-xxxhdpi/oh_no_girl_hint_red.webp b/app/features/src/main/res/drawable-xxxhdpi/oh_no_girl_hint_red.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/oh_no_girl_hint_red.webp rename to app/features/src/main/res/drawable-xxxhdpi/oh_no_girl_hint_red.webp diff --git a/android/src/main/res/drawable-xxxhdpi/old_man_of_color_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/old_man_of_color_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/old_man_of_color_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/old_man_of_color_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/old_man_of_color_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/old_man_of_color_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/old_man_of_color_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/old_man_of_color_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/onboarding_boygrannygranpa.webp b/app/features/src/main/res/drawable-xxxhdpi/onboarding_boygrannygranpa.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/onboarding_boygrannygranpa.webp rename to app/features/src/main/res/drawable-xxxhdpi/onboarding_boygrannygranpa.webp diff --git a/android/src/main/res/drawable-xxxhdpi/order_onb_blue_handoutmedicine.webp b/app/features/src/main/res/drawable-xxxhdpi/order_onb_blue_handoutmedicine.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/order_onb_blue_handoutmedicine.webp rename to app/features/src/main/res/drawable-xxxhdpi/order_onb_blue_handoutmedicine.webp diff --git a/android/src/main/res/drawable-xxxhdpi/order_onb_pharmacist_blue.webp b/app/features/src/main/res/drawable-xxxhdpi/order_onb_pharmacist_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/order_onb_pharmacist_blue.webp rename to app/features/src/main/res/drawable-xxxhdpi/order_onb_pharmacist_blue.webp diff --git a/android/src/main/res/drawable-xxxhdpi/paragraph.webp b/app/features/src/main/res/drawable-xxxhdpi/paragraph.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/paragraph.webp rename to app/features/src/main/res/drawable-xxxhdpi/paragraph.webp diff --git a/android/src/main/res/drawable-xxxhdpi/pharmacist.webp b/app/features/src/main/res/drawable-xxxhdpi/pharmacist.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/pharmacist.webp rename to app/features/src/main/res/drawable-xxxhdpi/pharmacist.webp diff --git a/android/src/main/res/drawable-xxxhdpi/pharmacist_hint.webp b/app/features/src/main/res/drawable-xxxhdpi/pharmacist_hint.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/pharmacist_hint.webp rename to app/features/src/main/res/drawable-xxxhdpi/pharmacist_hint.webp diff --git a/android/src/main/res/drawable-xxxhdpi/pharmacy_small.webp b/app/features/src/main/res/drawable-xxxhdpi/pharmacy_small.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/pharmacy_small.webp rename to app/features/src/main/res/drawable-xxxhdpi/pharmacy_small.webp diff --git a/android/src/main/res/drawable-xxxhdpi/prescription.webp b/app/features/src/main/res/drawable-xxxhdpi/prescription.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/prescription.webp rename to app/features/src/main/res/drawable-xxxhdpi/prescription.webp diff --git a/android/src/main/res/drawable-xxxhdpi/share_sheet.webp b/app/features/src/main/res/drawable-xxxhdpi/share_sheet.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/share_sheet.webp rename to app/features/src/main/res/drawable-xxxhdpi/share_sheet.webp diff --git a/android/src/main/res/drawable-xxxhdpi/truck_small.webp b/app/features/src/main/res/drawable-xxxhdpi/truck_small.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/truck_small.webp rename to app/features/src/main/res/drawable-xxxhdpi/truck_small.webp diff --git a/android/src/main/res/drawable-xxxhdpi/wheel_chair_user_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/wheel_chair_user_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/wheel_chair_user_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/wheel_chair_user_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/wheel_chair_user_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/wheel_chair_user_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/wheel_chair_user_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/wheel_chair_user_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_blue.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_blue.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_blue.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_red.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_red.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_red.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_red_shirt_circle_red.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_smartphone_circle_blue.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_smartphone_circle_blue.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_smartphone_circle_blue.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_smartphone_circle_blue.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_with_head_scarf_small_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_with_phone_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_with_phone_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_with_phone_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_with_phone_portrait.webp diff --git a/android/src/main/res/drawable-xxxhdpi/woman_with_phone_small_portrait.webp b/app/features/src/main/res/drawable-xxxhdpi/woman_with_phone_small_portrait.webp similarity index 100% rename from android/src/main/res/drawable-xxxhdpi/woman_with_phone_small_portrait.webp rename to app/features/src/main/res/drawable-xxxhdpi/woman_with_phone_small_portrait.webp diff --git a/android/src/main/res/drawable/ic_green_cross.xml b/app/features/src/main/res/drawable/ic_green_cross.xml similarity index 100% rename from android/src/main/res/drawable/ic_green_cross.xml rename to app/features/src/main/res/drawable/ic_green_cross.xml diff --git a/android/src/main/res/drawable/ic_healthcard.xml b/app/features/src/main/res/drawable/ic_healthcard.xml similarity index 100% rename from android/src/main/res/drawable/ic_healthcard.xml rename to app/features/src/main/res/drawable/ic_healthcard.xml diff --git a/android/src/main/res/drawable/ic_healthcard_spinner.xml b/app/features/src/main/res/drawable/ic_healthcard_spinner.xml similarity index 100% rename from android/src/main/res/drawable/ic_healthcard_spinner.xml rename to app/features/src/main/res/drawable/ic_healthcard_spinner.xml diff --git a/android/src/main/res/drawable/ic_healthcard_tag_lost.xml b/app/features/src/main/res/drawable/ic_healthcard_tag_lost.xml similarity index 100% rename from android/src/main/res/drawable/ic_healthcard_tag_lost.xml rename to app/features/src/main/res/drawable/ic_healthcard_tag_lost.xml diff --git a/android/src/main/res/drawable/ic_logo.xml b/app/features/src/main/res/drawable/ic_logo.xml similarity index 100% rename from android/src/main/res/drawable/ic_logo.xml rename to app/features/src/main/res/drawable/ic_logo.xml diff --git a/android/src/main/res/drawable/ic_logo_outlined.xml b/app/features/src/main/res/drawable/ic_logo_outlined.xml similarity index 100% rename from android/src/main/res/drawable/ic_logo_outlined.xml rename to app/features/src/main/res/drawable/ic_logo_outlined.xml diff --git a/android/src/main/res/drawable/ic_onboarding_logo_flag.xml b/app/features/src/main/res/drawable/ic_onboarding_logo_flag.xml similarity index 100% rename from android/src/main/res/drawable/ic_onboarding_logo_flag.xml rename to app/features/src/main/res/drawable/ic_onboarding_logo_flag.xml diff --git a/android/src/main/res/drawable/ic_onboarding_logo_gematik.xml b/app/features/src/main/res/drawable/ic_onboarding_logo_gematik.xml similarity index 100% rename from android/src/main/res/drawable/ic_onboarding_logo_gematik.xml rename to app/features/src/main/res/drawable/ic_onboarding_logo_gematik.xml diff --git a/android/src/main/res/drawable/ic_order_egk.xml b/app/features/src/main/res/drawable/ic_order_egk.xml similarity index 100% rename from android/src/main/res/drawable/ic_order_egk.xml rename to app/features/src/main/res/drawable/ic_order_egk.xml diff --git a/android/src/main/res/drawable/ic_phone_transparent.xml b/app/features/src/main/res/drawable/ic_phone_transparent.xml similarity index 100% rename from android/src/main/res/drawable/ic_phone_transparent.xml rename to app/features/src/main/res/drawable/ic_phone_transparent.xml diff --git a/android/src/main/res/drawable/ic_reset_pin.xml b/app/features/src/main/res/drawable/ic_reset_pin.xml similarity index 100% rename from android/src/main/res/drawable/ic_reset_pin.xml rename to app/features/src/main/res/drawable/ic_reset_pin.xml diff --git a/android/src/main/res/drawable/ic_tooltip_arrow_right.xml b/app/features/src/main/res/drawable/ic_tooltip_arrow_right.xml similarity index 100% rename from android/src/main/res/drawable/ic_tooltip_arrow_right.xml rename to app/features/src/main/res/drawable/ic_tooltip_arrow_right.xml diff --git a/android/src/main/res/drawable/ic_tooltip_arrow_top.xml b/app/features/src/main/res/drawable/ic_tooltip_arrow_top.xml similarity index 100% rename from android/src/main/res/drawable/ic_tooltip_arrow_top.xml rename to app/features/src/main/res/drawable/ic_tooltip_arrow_top.xml diff --git a/app/features/src/main/res/font/noto_sans_bold.ttf b/app/features/src/main/res/font/noto_sans_bold.ttf new file mode 100644 index 00000000..1db7886e Binary files /dev/null and b/app/features/src/main/res/font/noto_sans_bold.ttf differ diff --git a/app/features/src/main/res/font/noto_sans_medium.ttf b/app/features/src/main/res/font/noto_sans_medium.ttf new file mode 100644 index 00000000..5dbefd37 Binary files /dev/null and b/app/features/src/main/res/font/noto_sans_medium.ttf differ diff --git a/app/features/src/main/res/font/noto_sans_regular.ttf b/app/features/src/main/res/font/noto_sans_regular.ttf new file mode 100644 index 00000000..0a01a062 Binary files /dev/null and b/app/features/src/main/res/font/noto_sans_regular.ttf differ diff --git a/app/features/src/main/res/font/noto_sans_semibold.ttf b/app/features/src/main/res/font/noto_sans_semibold.ttf new file mode 100644 index 00000000..8b7fd130 Binary files /dev/null and b/app/features/src/main/res/font/noto_sans_semibold.ttf differ diff --git a/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..be316184 --- /dev/null +++ b/app/features/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/features/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/features/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..2341c4e9 Binary files /dev/null and b/app/features/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/features/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/app/features/src/main/res/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..a1f0e0a9 Binary files /dev/null and b/app/features/src/main/res/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/app/features/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/features/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..a28f9bf8 Binary files /dev/null and b/app/features/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/features/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..4b1a97a8 Binary files /dev/null and b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..9ff6276d Binary files /dev/null and b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..9cc2619b Binary files /dev/null and b/app/features/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..753501ab Binary files /dev/null and b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 00000000..0052b1c0 Binary files /dev/null and b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000..7aed4506 Binary files /dev/null and b/app/features/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/features/src/main/res/raw/analytics_identifier.json b/app/features/src/main/res/raw/analytics_identifier.json new file mode 100644 index 00000000..9518c43c --- /dev/null +++ b/app/features/src/main/res/raw/analytics_identifier.json @@ -0,0 +1,487 @@ +[ + { + "main": { + "name": "main" + } + }, + { + "main_createProfile": { + "name": "main:createProfile" + } + }, + { + "main_editProfilePicture": { + "name": "main:editProfilePicture" + } + }, + { + "main_editName": { + "name": "main:editName" + } + }, + { + "main_scanner": { + "name": "main:scanner" + } + }, + { + "main_deviceSecurity": { + "name": "main:deviceSecurity" + } + }, + { + "main_integrityWarning": { + "name": "main:integrityWarning" + } + }, + { + "main_prescriptionArchive": { + "name": "main:prescriptionArchive" + } + }, + { + "main_welcomeDrawer": { + "name": "main:welcomeDrawer" + } + }, + { + "prescriptionDetail": { + "name": "prescriptionDetail" + } + }, + { + "prescriptionDetail_medication": { + "name": "prescriptionDetail:medication" + } + }, + { + "prescriptionDetail_patient": { + "name": "prescriptionDetail:patient" + } + }, + { + "prescriptionDetail_practitioner": { + "name": "prescriptionDetail:practitioner" + } + }, + { + "prescriptionDetail_organization": { + "name": "prescriptionDetail:organization" + } + }, + { + "prescriptionDetail_accidentInfo": { + "name": "prescriptionDetail:accidentInfo" + } + }, + { + "prescriptionDetail_technicalInfo": { + "name": "prescriptionDetail:technicalInfo" + } + }, + { + "prescriptionDetail_sharePrescription": { + "name": "prescriptionDetail:sharePrescription" + } + }, + { + "prescriptionDetail_directAssignmentInfo": { + "name": "prescriptionDetail:directAssignmentInfo" + } + }, + { + "prescriptionDetail_substitutionInfo": { + "name": "prescriptionDetail:substitutionInfo" + } + }, + { + "prescriptionDetail_errorInfo": { + "name": "prescriptionDetail:errorInfo" + } + }, + { + "prescriptionDetail_prescriptionValidityInfo": { + "name": "prescriptionDetail:prescriptionValidityInfo" + } + }, + { + "prescriptionDetail_scannedPrescriptionInfo": { + "name": "prescriptionDetail:scannedPrescriptionInfo" + } + }, + { + "prescriptionDetail_coPaymentInfo": { + "name": "prescriptionDetail:coPaymentInfo" + } + }, + { + "prescriptionDetail_emergencyServiceFeeInfo": { + "name": "prescriptionDetail:emergencyServiceFeeInfo" + } + }, + { + "prescriptionDetail_medicationOverview": { + "name": "prescriptionDetail:medicationOverview" + } + }, + { + "prescriptionDetail_medication_ingredients": { + "name": "prescriptionDetail:medication_ingredients" + } + }, + { + "redeem_methodSelection": { + "name": "redeem:methodSelection" + } + }, + { + "redeem_prescriptionAllOrSelection": { + "name": "redeem:prescriptionAllOrSelection" + } + }, + { + "redeem_prescriptionChooseSubset": { + "name": "redeem:prescriptionChooseSubset" + } + }, + { + "redeem_matrixCode": { + "name": "redeem:matrixCode" + } + }, + { + "redeem_viaAVS": { + "name": "redeem:viaAVS" + } + }, + { + "redeem_viaTI": { + "name": "redeem:viaTI" + } + }, + { + "redeem_success": { + "name": "redeem:success" + } + }, + { + "redeem_editContactInformation": { + "name": "redeem:editContactInformation" + } + }, + { + "pharmacySearch": { + "name": "pharmacySearch" + } + }, + { + "pharmacySearch_detail": { + "name": "pharmacySearch:detail" + } + }, + { + "pharmacySearch_filter": { + "name": "pharmacySearch:filter" + } + }, + { + "pharmacySearch_map": { + "name": "pharmacySearch:map" + } + }, + { + "pharmacySearch_selectedPharmacy": { + "name": "pharmacySearch:selectedPharmacy" + } + }, + { + "cardWall": { + "name": "cardWall" + } + }, + { + "cardWall_introduction": { + "name": "cardWall:welcome" + } + }, + { + "cardWall_notCapable": { + "name": "cardWall:notCapable" + } + }, + { + "cardWall_CAN": { + "name": "cardWall:CAN" + } + }, + { + "cardWall_PIN": { + "name": "cardWall:PIN" + } + }, + { + "cardWall_scanCAN": { + "name": "cardWall:scanCAN" + } + }, + { + "cardWall_saveLogin": { + "name": "cardWall:saveCredentials:initial" + } + }, + { + "cardWall_saveLoginSecurityInfo": { + "name": "cardWall:saveCredentials:information" + } + }, + { + "cardWall_readCard": { + "name": "cardWall:connect" + } + }, + { + "cardWall_extAuth": { + "name": "cardWall:extAuth" + } + }, + { + "cardWall_extAuthConfirm": { + "name": "cardWall:extAuthConfirm" + } + }, + { + "troubleShooting": { + "name": "troubleShooting" + } + }, + { + "troubleShooting_readCardHelp1": { + "name": "troubleShooting:readCardHelp1" + } + }, + { + "troubleShooting_readCardHelp2": { + "name": "troubleShooting:readCardHelp2" + } + }, + { + "troubleShooting_readCardHelp3": { + "name": "troubleShooting:readCardHelp3" + } + }, + { + "contactInsuranceCompany": { + "name": "contactInsuranceCompany" + } + }, + { + "contactInsuranceCompany_selectKK": { + "name": "contactInsuranceCompany:selectKK" + } + }, + { + "contactInsuranceCompany_selectReason": { + "name": "contactInsuranceCompany:selectReason" + } + }, + { + "contactInsuranceCompany_selectMethod": { + "name": "contactInsuranceCompany:selectMethod" + } + }, + { + "orders": { + "name": "orders" + } + }, + { + "orders_detail": { + "name": "orders:detail" + } + }, + { + "orders_pickupCode": { + "name": "orders:pickupCode" + } + }, + { + "alert": { + "name": "General Alert Dialog" + } + }, + { + "errorAlert": { + "name": "Error Alert" + } + }, + { + "healthCardPassword_forgotPin": { + "name": "healthCardPassword:forgotPin" + } + }, + { + "healthCardPassword_setCustomPin": { + "name": "healthCardPassword:setCustomPin" + } + }, + { + "healthCardPassword_unlockCard": { + "name": "healthCardPassword:unlockCard" + } + }, + { + "healthCardPassword_introduction": { + "name": "healthCardPassword:introduction" + } + }, + { + "healthCardPassword_can": { + "name": "healthCardPassword:can" + } + }, + { + "healthCardPassword_puk": { + "name": "healthCardPassword:puk" + } + }, + { + "healthCardPassword_oldPin": { + "name": "healthCardPassword:oldPin" + } + }, + { + "healthCardPassword_pin": { + "name": "healthCardPassword:pin" + } + }, + { + "healthCardPassword_readCard": { + "name": "healthCardPassword:readCard" + } + }, + { + "healthCardPassword_scanner": { + "name": "healthCardPassword:scanner" + } + }, + { + "settings": { + "name": "settings" + } + }, + { + "settings_accessibility": { + "name": "settings:accessibility" + } + }, + { + "settings_authenticationMethods": { + "name": "settings:authenticationMethods" + } + }, + { + "settings_authenticationMethods_setAppPassword": { + "name": "settings:authenticationMethods:setAppPassword" + } + }, + { + "settings_productImprovements": { + "name": "settings:productImprovements" + } + }, + { + "settings_productImprovements_complyTracking": { + "name": "settings:productImprovements:complyTracking" + } + }, + { + "settings_legalNotice": { + "name": "settings:legalNotice" + } + }, + { + "settings_dataProtection": { + "name": "settings:dataProtection" + } + }, + { + "settings_openSourceLicence": { + "name": "settings:openSourceLicence" + } + }, + { + "settings_additionalLicence": { + "name": "settings:additionalLicence" + } + }, + { + "settings_termsOfUse": { + "name": "settings:termsOfUse" + } + }, + { + "profile": { + "name": "profile" + } + }, + { + "profile_editPicture": { + "name": "profile:editPicture" + } + }, + { + "profile_editPicture_imageCropper": { + "name": "profile:editPicture:imageCropper" + } + }, + { + "settings_newProfile": { + "name": "settings:newProfile" + } + }, + { + "profile_token": { + "name": "profile:token" + } + }, + { + "profile_registeredDevices": { + "name": "profile:registeredDevices" + } + }, + { + "profile_auditEvents": { + "name": "profile:auditEvents" + } + }, + { + "chargeItem_list": { + "name": "chargeItem:list" + } + }, + { + "chargeItem_details": { + "name": "chargeItem:details" + } + }, + { + "chargeItem_details_expanded": { + "name": "chargeItem:details:expanded" + } + }, + { + "chargeItem_share": { + "name": "chargeItem:share" + } + }, + { + "mlKit": { + "name": "mlKit" + } + }, + { + "mlKit_information": { + "name": "mlKit:information" + } + } +] diff --git a/app/features/src/main/res/raw/animation_courier.webm b/app/features/src/main/res/raw/animation_courier.webm new file mode 100644 index 00000000..449c50b1 Binary files /dev/null and b/app/features/src/main/res/raw/animation_courier.webm differ diff --git a/app/features/src/main/res/raw/animation_local.webm b/app/features/src/main/res/raw/animation_local.webm new file mode 100644 index 00000000..3e1f93e3 Binary files /dev/null and b/app/features/src/main/res/raw/animation_local.webm differ diff --git a/app/features/src/main/res/raw/animation_mail.webm b/app/features/src/main/res/raw/animation_mail.webm new file mode 100644 index 00000000..871ffac0 Binary files /dev/null and b/app/features/src/main/res/raw/animation_mail.webm differ diff --git a/app/features/src/main/res/raw/animation_pulse_lottie.json b/app/features/src/main/res/raw/animation_pulse_lottie.json new file mode 100644 index 00000000..80cf57e7 --- /dev/null +++ b/app/features/src/main/res/raw/animation_pulse_lottie.json @@ -0,0 +1 @@ +{"v":"5.1.16","fr":30,"ip":0,"op":60,"w":360,"h":360,"nm":"Pre-comp 2","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":16,"s":[0],"e":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":46,"s":[40],"e":[0]},{"t":76.0000030955435}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.309,99.021,0],"ix":2},"a":{"a":0,"k":[0.309,-0.979,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":16,"s":[0,0,100],"e":[100,100,100]},{"t":76.0000030955435}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[148.156,148.156],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2627450980392157,0.6,0.8823529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.309,-0.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":16.0000006516934,"op":76.0000030955435,"st":16.0000006516934,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":30,"s":[40],"e":[0]},{"t":60.0000024438501}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.309,99.021,0],"ix":2},"a":{"a":0,"k":[0.309,-0.979,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"t":60.0000024438501}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[148.156,148.156],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2627450980392157,0.6,0.8823529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.309,-0.979],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":76.0000030955435,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180,180,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":0,"op":39.0000015885026,"st":-37.0000015070409,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180,180,0],"ix":2},"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"w":200,"h":200,"ip":23.0000009368092,"op":60.0000024438501,"st":23.0000009368092,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/features/src/main/res/raw/device_lottie.json b/app/features/src/main/res/raw/device_lottie.json new file mode 100644 index 00000000..8b9f49fb --- /dev/null +++ b/app/features/src/main/res/raw/device_lottie.json @@ -0,0 +1 @@ +{"v":"5.6.6","ip":0,"op":1,"fr":60,"w":241,"h":146,"layers":[{"ind":1899,"nm":"surface8209","ao":0,"ip":0,"op":60,"st":0,"ty":4,"ks":{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[133.33,133.33]},"sk":{"k":0},"sa":{"k":0}},"shapes":[{"ty":"gr","hd":false,"nm":"surface8209","it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.05,0.06],[0,0.08],[-0.05,0.06],[-0.07,0.02],[-0.21,0],[-0.19,-0.1],[-0.05,-0.07],[0,-0.08],[0.05,-0.07],[0.08,-0.02],[0.21,0],[0.19,0.1]],"o":[[-0.07,-0.02],[-0.05,-0.07],[0,-0.08],[0.05,-0.07],[0.19,-0.1],[0.21,0],[0.08,0.02],[0.05,0.06],[0,0.08],[-0.05,0.06],[-0.19,0.1],[-0.21,0],[0,0]],"v":[[149.08,18.38],[148.89,18.25],[148.82,18.03],[148.89,17.81],[149.08,17.67],[149.7,17.52],[150.31,17.67],[150.5,17.81],[150.57,18.03],[150.5,18.25],[150.31,18.38],[149.7,18.53],[149.08,18.38]],"c":true}}},{"ty":"fl","o":{"k":10},"c":{"k":[0.26,0.6,0.88,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0.66],[1.15,0],[0,-0.66],[-1.15,0]],"o":[[1.15,0],[0,-0.66],[-1.15,0],[0,0.66],[0,0]],"v":[[149.7,19.23],[151.78,18.03],[149.7,16.82],[147.62,18.03],[149.7,19.23]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.75,0.89,0.97,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[2.75,1.16],[0.27,0.15],[0,0],[0.41,0.55],[-2.07,1.19],[0,0],[-2.75,-1.59],[0,0],[0.01,-0.01],[-2.14,-1.23],[-2.13,1.23],[0,0],[0,0],[2.75,-1.59]],"o":[[0,0],[-2.48,1.43],[-0.29,-0.12],[0,0],[-0.61,-0.32],[-1.04,-1.47],[0,0],[2.75,-1.59],[0,0],[-0.02,0],[-2.14,1.23],[2.14,1.23],[0,0],[0,0],[2.75,1.59],[0,0]],"v":[[172.89,35.74],[63.87,98.71],[54.75,99.12],[53.92,98.71],[10,73.32],[8.45,71.99],[10,67.58],[119.02,4.64],[128.97,4.64],[147.06,15.09],[147.02,15.11],[147.02,19.57],[154.76,19.57],[154.8,19.54],[172.89,29.99],[172.89,35.74]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.26,0.26,0.26,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0],[2.14,1.23],[-2.14,1.23],[0,0],[0,0],[2.75,-1.59],[0,0],[-2.75,-1.58],[0,0],[-2.75,1.59],[0,0],[2.75,1.59]],"o":[[0,0],[0,0],[-2.14,1.23],[-2.14,-1.23],[0,0],[0,0],[-2.75,-1.59],[0,0],[-2.75,1.59],[0,0],[2.75,1.59],[0,0],[2.75,-1.58],[0,0]],"v":[[172.89,30],[154.8,19.55],[154.75,19.57],[147.02,19.57],[147.02,15.11],[147.06,15.07],[128.97,4.62],[119.02,4.62],[9.99,67.58],[9.99,73.32],[53.91,98.69],[63.86,98.69],[172.89,35.74],[172.89,30]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[1,1,1,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.27],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.61],[0,0],[3.93,2.27],[0,0],[-4.54,2.62],[0,0]],"v":[[49.51,103.04],[3.17,76.27],[4.29,67.43],[116.21,2.8],[131.52,2.16],[177.87,28.93],[176.75,37.77],[64.83,102.4],[49.51,103.04]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0.5],[-0.11,0.21],[-0.2,0.14],[0,0],[0,-0.5],[0.11,-0.21],[0.2,-0.14]],"o":[[0,0],[-0.37,0.21],[0,-0.24],[0.11,-0.22],[0,0],[0.37,-0.21],[0,0.24],[-0.11,0.21],[0,0]],"v":[[157.77,52.09],[152.07,55.38],[151.41,54.88],[151.59,54.18],[152.07,53.64],[157.77,50.35],[158.41,50.84],[158.24,51.54],[157.77,52.09]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[1,0.7,0.7,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0.49],[-0.11,0.22],[-0.2,0.14],[0,0],[0,-0.5],[0.11,-0.21],[0.2,-0.14]],"o":[[0,0],[-0.37,0.21],[0,-0.25],[0.11,-0.21],[0,0],[0.38,-0.21],[0,0.24],[-0.11,0.22],[0,0]],"v":[[166.49,47.04],[160.79,50.32],[160.14,49.83],[160.32,49.13],[160.79,48.58],[166.49,45.29],[167.14,45.79],[166.97,46.49],[166.49,47.04]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.96,0.96,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0.12,0.23],[0,0.26],[-0.39,-0.21],[0,0],[-0.12,-0.23],[0,-0.26],[0.39,0.22]],"o":[[0,0],[-0.21,-0.15],[-0.12,-0.23],[0,-0.52],[0,0],[0.21,0.15],[0.12,0.23],[0,0.52],[0,0]],"v":[[27.67,93.77],[21.54,90.23],[21.03,89.64],[20.84,88.89],[21.54,88.36],[27.67,91.9],[28.18,92.48],[28.37,93.24],[27.67,93.77]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[41.3,101.21],[41.3,100.21],[40.44,99.72],[40.44,100.71],[41.3,101.21]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[39.07,99.91],[39.07,98.92],[38.21,98.43],[38.21,99.42],[39.07,99.91]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[36.84,98.63],[36.84,97.64],[35.98,97.14],[35.98,98.14],[36.84,98.63]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.23,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[34.61,97.34],[34.61,96.34],[33.75,95.85],[33.75,96.84],[34.61,97.34]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[15.45,86.29],[15.46,85.29],[14.6,84.8],[14.59,85.79],[15.45,86.29]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[13.22,84.99],[13.23,84],[12.37,83.5],[12.36,84.5],[13.22,84.99]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[10.99,83.71],[11,82.71],[10.13,82.22],[10.13,83.21],[10.99,83.71]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0.24,0.41],[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14]],"o":[[0.24,-0.14],[-0.24,-0.41],[-0.24,0.14],[0.24,0.41],[0,0]],"v":[[8.76,82.41],[8.77,81.42],[7.9,80.93],[7.9,81.92],[8.76,82.41]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0,0,0,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.26],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.6],[0,0],[3.93,2.27],[0,0],[-4.54,2.61],[0,0]],"v":[[49.51,103.69],[3.17,76.93],[4.29,68.08],[116.21,3.46],[131.52,2.83],[177.87,29.59],[176.75,38.43],[64.83,103.06],[49.51,103.69]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.26,0.26,0.26,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[0.52,76.65],[0.52,72.39],[5.52,74.27]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[-4.55,2.62],[0,0],[-3.92,-2.27],[0,0],[4.55,-2.62],[0,0],[3.93,2.27]],"o":[[0,0],[-3.93,-2.27],[0,0],[4.54,-2.6],[0,0],[3.93,2.27],[0,0],[-4.54,2.62],[0,0]],"v":[[49.51,107.3],[3.17,80.54],[4.29,71.7],[116.21,7.08],[131.52,6.44],[177.87,33.21],[176.75,42.05],[64.83,106.67],[49.51,107.3]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[180.52,36.89],[180.52,32.79],[176.54,35.35]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.13,0.13,0.13,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]}]}],"meta":{"g":"LF SVG to Lottie"}} \ No newline at end of file diff --git a/android/src/main/res/raw/health_insurance_contacts.json b/app/features/src/main/res/raw/health_insurance_contacts.json similarity index 100% rename from android/src/main/res/raw/health_insurance_contacts.json rename to app/features/src/main/res/raw/health_insurance_contacts.json diff --git a/app/features/src/main/res/raw/healthcard_lottie.json b/app/features/src/main/res/raw/healthcard_lottie.json new file mode 100644 index 00000000..f4391347 --- /dev/null +++ b/app/features/src/main/res/raw/healthcard_lottie.json @@ -0,0 +1 @@ +{"v":"5.6.6","ip":0,"op":1,"fr":60,"w":172,"h":168,"layers":[{"ind":1426,"nm":"surface4347","ao":0,"ip":0,"op":60,"st":0,"ty":4,"ks":{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[133.33,133.33]},"sk":{"k":0},"sa":{"k":0}},"shapes":[{"ty":"gr","hd":false,"nm":"surface4347","it":[{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[-1.88,-1.02],[0,0],[1.99,-1.2],[0,0],[1.89,1.09],[0,0],[-1.94,1.19]],"o":[[1.82,-1.12],[0,0],[2.05,1.11],[0,0],[-1.88,1.12],[0,0],[-1.96,-1.14],[0,0]],"v":[[48.59,29.29],[54.58,29.13],[121.92,65.54],[122.04,70.75],[76.98,97.8],[70.88,97.85],[6.3,60.38],[6.23,55.23]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.56,0.8,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"gr","hd":false,"it":[{"ty":"sh","ks":{"k":{"i":[[0,0],[-0.94,-0.51],[0,0],[1.99,-1.2],[0,0],[1.89,1.09],[0,0],[-1.94,1.19]],"o":[[0.91,-0.56],[0,0],[2.05,1.11],[0,0],[-1.87,1.12],[0,0],[-1.96,-1.14],[0,0]],"v":[[50.07,29.14],[53.06,29.05],[121.92,66.29],[122.04,71.5],[76.98,98.55],[70.88,98.6],[6.3,61.13],[6.24,55.98]],"c":true}}},{"ty":"fl","o":{"k":100},"c":{"k":[0.96,0.96,0.96,1]}},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]},{"ty":"tr","o":{"k":100},"r":{"k":0},"p":{"k":[0,0]},"a":{"k":[0,0]},"s":{"k":[100,100]},"sk":{"k":0},"sa":{"k":0},"hd":false}]}]}],"meta":{"g":"LF SVG to Lottie"}} \ No newline at end of file diff --git a/app/features/src/main/res/raw/nfc_positions.json b/app/features/src/main/res/raw/nfc_positions.json new file mode 100644 index 00000000..1cc87b42 --- /dev/null +++ b/app/features/src/main/res/raw/nfc_positions.json @@ -0,0 +1,1713 @@ +[ + { + "manufacturer": "Huawei", + "marketingName": "HONOR 10", + "modelNames": [ + "COL-AL00", + "COL-AL10", + "COL-L29", + "COL-TL10" + ], + "nfcPos": { + "x0": 0.07851239669421484, + "y0": 0.011210762331838564, + "x1": 0.5165289256198347, + "y1": 0.09865470852017937 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR 20", + "modelNames": [ + "YAL-AL00", + "YAL-L21", + "YAL-TL00" + ], + "nfcPos": { + "x0": 0.265625, + "y0": 0.014084507042253521, + "x1": 0.7135416666666667, + "y1": 0.15023474178403756 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR 9", + "modelNames": [ + "STF-AL00", + "STF-AL10", + "STF-L09", + "STF-L09S", + "STF-TL10" + ], + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.4530386740331491, + "y1": 0.08735632183908046 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR Magic 2", + "modelNames": [ + "TNY-AL00", + "TNY-TL00" + ], + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.6772486772486772, + "y1": 0.10304449648711944 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR Note10", + "modelNames": [ + "RVL-AL09" + ], + "nfcPos": { + "x0": 0.17803030303030298, + "y0": 0.0044943820224719105, + "x1": 0.7992424242424243, + "y1": 0.056179775280898875 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR Play", + "modelNames": [ + "COR-AL00", + "COR-AL10", + "COR-L29", + "COR-TL10" + ], + "nfcPos": { + "x0": 0.042857142857142816, + "y0": 0.020179372197309416, + "x1": 0.6761904761904762, + "y1": 0.12556053811659193 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR V10", + "modelNames": [ + "BKL-AL00", + "BKL-AL20", + "BKL-TL10" + ], + "nfcPos": { + "x0": 0.07851239669421484, + "y0": 0.011210762331838564, + "x1": 0.5165289256198347, + "y1": 0.09865470852017937 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR V20", + "modelNames": [ + "PCT-TL10" + ], + "nfcPos": { + "x0": 0.3121693121693122, + "y0": 0.07621247113163972, + "x1": 0.5873015873015873, + "y1": 0.19630484988452657 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HONOR V9", + "modelNames": [ + "DUK-AL20", + "DUK-TL30" + ], + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.4530386740331491, + "y1": 0.08735632183908046 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate 10-Serie", + "modelNames": [ + "ALP-AL00", + "ALP-L09", + "ALP-L29", + "ALP-TL00", + "BLA-A09", + "BLA-AL00", + "BLA-L09", + "BLA-L29", + "BLA-TL00", + "RNE-L01", + "RNE-L03", + "RNE-L21", + "RNE-L23" + ], + "nfcPos": { + "x0": 0.1701030927835051, + "y0": 0.0, + "x1": 0.7731958762886598, + "y1": 0.12413793103448276 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate 20-Serie", + "modelNames": [ + "HMA-L09", + "HMA-L29", + "LYA-L0C", + "LYA-L29", + "EVR-AN00", + "EVR-N29", + "SNE-LX1", + "EVR-L29", + "EVR-TL00", + "EVR-N29", + "EVR-AL00", + "HMA-AL00", + "HMA-L09", + "HMA-L29", + "HMA-TL00", + "LYA-AL00", + "LYA-AL10", + "LYA-L09", + "LYA-L29", + "LYA-TL00", + "LYA-AL00P", + "SNE-LX1", + "SNE-LX2", + "SNE-LX3" + ], + "nfcPos": { + "x0": 0.2328042328042328, + "y0": 0.10161662817551963, + "x1": 0.7671957671957672, + "y1": 0.2678983833718245 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate 30-Serie", + "modelNames": [ + "TAS-L09", + "TAS-L29", + "TAS-AL00", + "TAS-TL00", + "LIO-L09", + "LIO-L29", + "LIO-AL00", + "LIO-TL00", + "LIO-N29", + "LIO-AL10", + "LIO-TL10", + "TAS-AN00", + "TAS-TN00", + "LIO-AN00m", + "SPL-AL00", + "SPL-TL00", + "LIO-N29", + "LIO-AN00P", + "LIO-AN00" + ], + "nfcPos": { + "x0": 0.12903225806451613, + "y0": 0.020833333333333332, + "x1": 0.8333333333333334, + "y1": 0.3055555555555556 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate 40-Serie", + "modelNames": [ + "NOH-NX9", + "NOH-AN00", + "NOH-AN01", + "NOP-AN00", + "OCE-AN10", + "NOH-AL00", + "OCE-AN50", + "NOP-AN00", + "OCE-AL50" + ], + "nfcPos": { + "x0": 0.08860759493670889, + "y0": 0.012587412587412588, + "x1": 0.9113924050632911, + "y1": 0.35664335664335667 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate 9-Serie", + "modelNames": [ + "BLL-L23", + "HUAWEI BLL-L23", + "MHA-AL00", + "MHA-L09", + "MHA-L29", + "MHA-TL00", + "LON-AL00", + "LON-L29" + ], + "nfcPos": { + "x0": 0.17431192660550454, + "y0": 0.0022935779816513763, + "x1": 0.8027522935779816, + "y1": 0.06422018348623854 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate RS", + "modelNames": [ + "NEO-AL00", + "NEO-L29" + ], + "nfcPos": { + "x0": 0.13440860215053763, + "y0": 0.02947845804988662, + "x1": 0.8763440860215054, + "y1": 0.1836734693877551 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI Mate X-Serie", + "modelNames": [ + "TAH-AN00", + "TAH-N29m" + ], + "nfcPos": { + "x0": 0.023400936037441533, + "y0": 0.0, + "x1": 0.37597503900156004, + "y1": 0.04741980474198047 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI nova 2s", + "modelNames": [ + "HWI-AL00", + "HWI-TL00" + ], + "nfcPos": { + "x0": 0.07106598984771573, + "y0": 0.0022988505747126436, + "x1": 0.5076142131979695, + "y1": 0.12413793103448276 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI nova 3-Serie", + "modelNames": [ + "INE-LX1", + "INE-LX2r", + "PAR-AL00", + "PAR-L21", + "PAR-L29", + "PAR-LX1", + "PAR-LX1M", + "PAR-LX9", + "PAR-TL00", + "PAR-TL20", + "ANE-AL00", + "ANE-TL00", + "INE-AL00", + "INE-LX1", + "INE-LX1r", + "INE-LX2", + "INE-TL00" + ], + "nfcPos": { + "x0": 0.0, + "y0": 0.0, + "x1": 0.7679558011049724, + "y1": 0.0979020979020979 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI P10-Serie", + "modelNames": [ + "VTR-AL00", + "VTR-L09", + "VTR-L29", + "VTR-TL00", + "VKY-AL00", + "VKY-L09", + "VKY-L29", + "VKY-TL00", + "WAS-L03T", + "WAS-LX1", + "WAS-LX1A", + "WAS-LX2", + "WAS-LX2J", + "WAS-LX3" + ], + "nfcPos": { + "x0": 0.1707317073170732, + "y0": 0.0, + "x1": 0.5951219512195122, + "y1": 0.07209302325581396 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI P20-Serie", + "modelNames": [ + "ANE-LX2J", + "HWV32", + "EML-AL00", + "EML-L09", + "EML-L29", + "EML-TL00", + "HW-01K", + "CLT-AL00", + "CLT-AL00l", + "CLT-AL01", + "CLT-L04", + "CLT-L09", + "CLT-L29", + "CLT-TL00", + "CLT-TL01", + "ANE-LX1", + "ANE-LX2", + "ANE-LX3", + "CLT-L09", + "CLT-L29" + ], + "nfcPos": { + "x0": 0.0871794871794872, + "y0": 0.02546296296296296, + "x1": 0.7128205128205128, + "y1": 0.2708333333333333 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI P30-Serie", + "modelNames": [ + "ELE-AL00", + "ELE-L04", + "ELE-L09", + "ELE-L14", + "ELE-L29", + "ELE-L39", + "ELE-L49", + "ELE-TL00", + "HWV33", + "MAR-LX1A", + "MAR-LX1Am", + "MAR-LX1B", + "MAR-LX1M", + "MAR-LX1Mm", + "MAR-LX2", + "MAR-LX2B", + "MAR-LX2m", + "MAR-LX3A", + "MAR-LX3Am", + "MAR-LX3Bm", + "ELE-L09", + "HW-02L", + "VOG-AL00", + "VOG-AL10", + "VOG-L04", + "VOG-L09", + "VOG-L29", + "VOG-TL00", + "MAR-LX2J" + ], + "nfcPos": { + "x0": 0.07853403141361259, + "y0": 0.020737327188940093, + "x1": 0.6073298429319371, + "y1": 0.2557603686635945 + } + }, + { + "manufacturer": "Huawei", + "marketingName": "HUAWEI P40-Serie", + "modelNames": [ + "ELS-NX9", + "ELS-N04", + "ELS-AN00", + "ELS-TN00", + "JNY-L21A", + "JNY-L01A", + "JNY-L21B", + "JNY-L22A", + "JNY-L02A", + "JNY-L22B", + "JNY-LX1", + "ANA-AN00", + "ANA-TN00", + "ANA-NX9", + "ANA-LX4", + "ELS-N39", + "ELS-AN10", + "CDY-NX9A", + "ANA-AL00", + "ART-L28", + "ART-L29", + "ART-L29N" + ], + "nfcPos": { + "x0": 0.032573289902280145, + "y0": 0.04495912806539509, + "x1": 0.501628664495114, + "y1": 0.3201634877384196 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A32 5G", + "modelNames": [ + "SCG08", + "SM-A326B", + "SM-A326BR", + "SM-A326U", + "SM-A326U1", + "SM-A326W", + "SM-S326DL" + ], + "nfcPos": { + "x0": 0.0699300699300699, + "y0": 0.29354838709677417, + "x1": 0.8951048951048951, + "y1": 0.603225806451613 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A42 5G", + "modelNames": [ + "SM-A4260", + "SM-A426B", + "SM-A426N", + "SM-A426U", + "SM-A426U1", + "SM-S426DL" + ], + "nfcPos": { + "x0": 0.049645390070921946, + "y0": 0.26129032258064516, + "x1": 0.9290780141843972, + "y1": 0.6903225806451613 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A50s", + "modelNames": [ + "SM-A5070", + "SM-A507FN" + ], + "nfcPos": { + "x0": 0.217687074829932, + "y0": 0.1064516129032258, + "x1": 0.7006802721088435, + "y1": 0.3064516129032258 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A51", + "modelNames": [ + "SM-A515F", + "SM-A515U", + "SM-A515U1", + "SM-A515W", + "SM-S515DL" + ], + "nfcPos": { + "x0": 0.08450704225352113, + "y0": 0.08108108108108109, + "x1": 0.6056338028169015, + "y1": 0.2905405405405405 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A52 5G", + "modelNames": [ + "SC-53B", + "SM-A5260", + "SM-A526B", + "SM-A526N", + "SM-A526U", + "SM-A526U1", + "SM-A526W" + ], + "nfcPos": { + "x0": 0.03597122302158273, + "y0": 0.0, + "x1": 0.5611510791366907, + "y1": 0.28378378378378377 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A60", + "modelNames": [ + "SM-A6060", + "SM-A606Y" + ], + "nfcPos": { + "x0": 0.1842105263157895, + "y0": 0.13306451612903225, + "x1": 0.7456140350877193, + "y1": 0.3225806451612903 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A70", + "modelNames": [ + "SM-A7050", + "SM-A705F", + "SM-A705FN", + "SM-A705GM", + "SM-A705MN", + "SM-A705U", + "SM-A705W", + "SM-A705YN" + ], + "nfcPos": { + "x0": 0.2206896551724138, + "y0": 0.12903225806451613, + "x1": 0.7655172413793103, + "y1": 0.3064516129032258 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A71", + "modelNames": [ + "SM-A715F", + "SM-A715W" + ], + "nfcPos": { + "x0": 0.07638888888888884, + "y0": 0.050335570469798654, + "x1": 0.5972222222222222, + "y1": 0.2684563758389262 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A8+", + "modelNames": [ + "SM-A730F", + "SM-A730X" + ], + "nfcPos": { + "x0": 0.1095890410958904, + "y0": 0.3076923076923077, + "x1": 0.8561643835616438, + "y1": 0.5737179487179487 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A8", + "modelNames": [ + "SCV32", + "SM-A800F", + "SM-A800YZ", + "SM-A800S", + "SM-A800I", + "SM-A800IZ", + "SM-A8000", + "SM-A800X", + "SM-G885F", + "SM-G885Y", + "SM-G8850", + "SM-G885S", + "SM-A810F", + "SM-A810YZ", + "SM-A810S", + "SM-A530F", + "SM-A530X", + "SM-A530W", + "SM-A530N" + ], + "nfcPos": { + "x0": 0.19424460431654678, + "y0": 0.06752411575562701, + "x1": 0.7769784172661871, + "y1": 0.21221864951768488 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A80", + "modelNames": [ + "SM-A8050", + "SM-A805F", + "SM-A805N" + ], + "nfcPos": { + "x0": 0.13013698630136983, + "y0": 0.18387096774193548, + "x1": 0.8356164383561644, + "y1": 0.47419354838709676 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A8s", + "modelNames": [ + "SM-G887F", + "SM-G8870" + ], + "nfcPos": { + "x0": 0.25, + "y0": 0.10240963855421686, + "x1": 0.7714285714285715, + "y1": 0.3072289156626506 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A9 (2018)", + "modelNames": [ + "SM-A920F", + "SM-A920N" + ], + "nfcPos": { + "x0": 0.04402515723270439, + "y0": 0.03115264797507788, + "x1": 0.9559748427672956, + "y1": 0.4143302180685358 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy C5 Pro", + "modelNames": [ + "SM-C5010", + "SM-C5018" + ], + "nfcPos": { + "x0": 0.23076923076923073, + "y0": 0.08012820512820513, + "x1": 0.6474358974358974, + "y1": 0.2403846153846154 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy C7 Pro", + "modelNames": [ + "SM-C701F", + "SM-C7010", + "SM-C7018" + ], + "nfcPos": { + "x0": 0.27338129496402874, + "y0": 0.0641025641025641, + "x1": 0.7122302158273381, + "y1": 0.21794871794871795 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy C9 Pro", + "modelNames": [ + "SM-C900F", + "SM-C900Y", + "SM-C9000", + "SM-C9008", + "SM-C900X" + ], + "nfcPos": { + "x0": 0.22857142857142854, + "y0": 0.07333333333333333, + "x1": 0.6928571428571428, + "y1": 0.20333333333333334 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Fold", + "modelNames": [ + "SCV44", + "SM-F9000", + "SM-F900F", + "SM-F900U", + "SM-F900U1", + "SM-F900W" + ], + "nfcPos": { + "x0": 0.15315315315315314, + "y0": 0.38387096774193546, + "x1": 0.9099099099099099, + "y1": 0.6387096774193548 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note10 Lite", + "modelNames": [ + "SM-N770F" + ], + "nfcPos": { + "x0": 0.10416666666666663, + "y0": 0.08389261744966443, + "x1": 0.5555555555555556, + "y1": 0.2953020134228188 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note10+", + "modelNames": [ + "SC-01M", + "SCV45", + "SM-N9750", + "SM-N975C", + "SM-N975U", + "SM-N975U1", + "SM-N975W", + "SM-N975F" + ], + "nfcPos": { + "x0": 0.09547738693467334, + "y0": 0.06129032258064516, + "x1": 0.542713567839196, + "y1": 0.35161290322580646 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note10", + "modelNames": [ + "SM-N970F", + "SM-N9700", + "SM-N970U", + "SM-N970U1", + "SM-N970W" + ], + "nfcPos": { + "x0": 0.08374384236453203, + "y0": 0.06129032258064516, + "x1": 0.5270935960591133, + "y1": 0.3419354838709677 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note20 5G", + "modelNames": [ + "SM-N9810", + "SM-N981N", + "SM-N981U", + "SM-N981U1", + "SM-N981W", + "SM-N981B" + ], + "nfcPos": { + "x0": 0.10516252390057357, + "y0": 0.4105263157894737, + "x1": 0.9082217973231358, + "y1": 0.7140350877192982 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note20 Ultra 5G", + "modelNames": [ + "SC-53A", + "SCG06", + "SM-N9860", + "SM-N986N", + "SM-N986U", + "SM-N986U1", + "SM-N986W", + "SM-N986B" + ], + "nfcPos": { + "x0": 0.06691449814126393, + "y0": 0.34509466437177283, + "x1": 0.9219330855018587, + "y1": 0.6858864027538726 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note5", + "modelNames": [ + "SM-N9208", + "SM-N920C", + "SM-N920F", + "SM-N920G", + "SM-N920I", + "SM-N920X", + "SM-N920R7", + "SAMSUNG-SM-N920A", + "SM-N920W8", + "SM-N9200", + "SM-N9208", + "SM-N9200", + "SM-N920K", + "SM-N920L", + "SM-N920R6", + "SM-N920S", + "SM-N920P", + "SM-N920T", + "SM-N920R4", + "SM-N920V" + ], + "nfcPos": { + "x0": 0.09219858156028371, + "y0": 0.4180064308681672, + "x1": 0.9787234042553191, + "y1": 0.77491961414791 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note8", + "modelNames": [ + "SC-01K", + "SCV37", + "SM-N950F", + "SM-N950N", + "SM-N950XN", + "SM-N950U", + "SM-N9500", + "SM-N9508", + "SM-N950W", + "SM-N950U1" + ], + "nfcPos": { + "x0": 0.18055555555555558, + "y0": 0.24666666666666667, + "x1": 0.8402777777777778, + "y1": 0.6266666666666667 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Note9", + "modelNames": [ + "SC-01L", + "SCV40", + "SM-N960F", + "SM-N960N", + "SM-N9600", + "SM-N960W", + "SM-N960U", + "SM-N960U1" + ], + "nfcPos": { + "x0": 0.23239436619718312, + "y0": 0.3389261744966443, + "x1": 0.823943661971831, + "y1": 0.5906040268456376 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S10+", + "modelNames": [ + "SC-04L", + "SCV42", + "SM-G975F", + "SM-G975N", + "SM-G9750", + "SM-G9758", + "SM-G975U", + "SM-G975U1", + "SM-G975W" + ], + "nfcPos": { + "x0": 0.1806451612903226, + "y0": 0.3383233532934132, + "x1": 0.8129032258064516, + "y1": 0.6347305389221557 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S10", + "modelNames": [ + "SC-03L", + "SCV41", + "SM-G973F", + "SM-G973N", + "SM-G9730", + "SM-G9738", + "SM-G973C", + "SM-G973U", + "SM-G973U1", + "SM-G973W" + ], + "nfcPos": { + "x0": 0.05031446540880502, + "y0": 0.2433234421364985, + "x1": 0.8050314465408805, + "y1": 0.6468842729970327 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S10e", + "modelNames": [ + "SM-G970F", + "SM-G970N", + "SM-G9700", + "SM-G9708", + "SM-G970U", + "SM-G970U1", + "SM-G970W" + ], + "nfcPos": { + "x0": 0.20370370370370372, + "y0": 0.322884012539185, + "x1": 0.8024691358024691, + "y1": 0.5987460815047022 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S20 FE", + "modelNames": [ + "SM-G780G", + "SM-G780F" + ], + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S20 Ultra", + "modelNames": [], + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S20+", + "modelNames": [ + "SM-G985F" + ], + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S20", + "modelNames": [ + "SM-G980F" + ], + "nfcPos": { + "x0": 0.0680628272251309, + "y0": 0.36764705882352944, + "x1": 0.9267015706806283, + "y1": 0.7271241830065359 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S21 5G", + "modelNames": [ + "SC-51B", + "SCG09", + "SM-G9910", + "SM-G991Q", + "SM-G991U1", + "SM-G991W", + "SM-G991B", + "SM-G991N" + ], + "nfcPos": { + "x0": 0.04929577464788737, + "y0": 0.46308724832214765, + "x1": 0.9436619718309859, + "y1": 0.7651006711409396 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S21 FE 5G", + "modelNames": [ + "SM-G9900", + "SM-G990B", + "SM-G990B2", + "SM-G990U", + "SM-G990U1", + "SM-G990U2", + "SM-G990U3", + "SM-G990W", + "SM-G990W2", + "SM-G990E" + ], + "nfcPos": { + "x0": 0.08904109589041098, + "y0": 0.28619528619528617, + "x1": 0.9178082191780822, + "y1": 0.6531986531986532 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S21 Ultra 5G", + "modelNames": [ + "SC-52B", + "SM-G9980", + "SM-G998U", + "SM-G998U1", + "SM-G998W", + "SM-G998B", + "SM-G998N" + ], + "nfcPos": { + "x0": 0.02877697841726623, + "y0": 0.40604026845637586, + "x1": 0.9568345323741008, + "y1": 0.7550335570469798 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S21+ 5G", + "modelNames": [ + "SCG10", + "SM-G9960", + "SM-G996U1", + "SM-G996W", + "SM-G996B", + "SM-G996N" + ], + "nfcPos": { + "x0": 0.035211267605633756, + "y0": 0.39932885906040266, + "x1": 0.971830985915493, + "y1": 0.7550335570469798 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S22 Ultra", + "modelNames": [ + "SC-52C", + "SCG14", + "SM-S9080", + "SM-S908E", + "SM-S908N", + "SM-S908U", + "SM-S908U1", + "SM-S908W", + "SM-S908B" + ], + "nfcPos": { + "x0": 0.007633587786259555, + "y0": 0.34838709677419355, + "x1": 1.0, + "y1": 0.7741935483870968 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S22+", + "modelNames": [ + "SM-S9060", + "SM-S906E", + "SM-S906N", + "SM-S906U", + "SM-S906U1", + "SM-S906W", + "SM-S906B" + ], + "nfcPos": { + "x0": 0.05405405405405406, + "y0": 0.3258064516129032, + "x1": 0.9594594594594594, + "y1": 0.7548387096774194 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S22", + "modelNames": [ + "SC-51C", + "SCG13", + "SM-S9010", + "SM-S901E", + "SM-S901N", + "SM-S901U", + "SM-S901U1", + "SM-S901W", + "SM-S901B" + ], + "nfcPos": { + "x0": 0.05921052631578949, + "y0": 0.35161290322580646, + "x1": 0.9144736842105263, + "y1": 0.7580645161290323 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S6 edge+", + "modelNames": [ + "SM-G9287", + "SM-G928F", + "SM-G928G" + ], + "nfcPos": { + "x0": 0.27922077922077926, + "y0": 0.36012861736334406, + "x1": 0.7272727272727273, + "y1": 0.7234726688102894 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S7 edge", + "modelNames": [ + "SM-G935F", + "SM-G935L", + "SM-G9350", + "SM-G935U" + ], + "nfcPos": { + "x0": 0.09999999999999998, + "y0": 0.255663430420712, + "x1": 0.9, + "y1": 0.627831715210356 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S7", + "modelNames": [ + "SM-G930F", + "SM-G930X", + "SM-G930W8", + "SM-G930K", + "SM-G930L", + "SM-G930S", + "SM-G930R7", + "SAMSUNG-SM-G930AZ", + "SAMSUNG-SM-G930A", + "SM-G930VC", + "SM-G9300", + "SM-G9308", + "SM-G930R6", + "SM-G930T1", + "SM-G930P", + "SM-G930VL", + "SM-G930T", + "SM-G930U", + "SM-G930R4", + "SM-G930V" + ], + "nfcPos": { + "x0": 0.06578947368421051, + "y0": 0.26282051282051283, + "x1": 0.9539473684210527, + "y1": 0.6217948717948718 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S8+", + "modelNames": [ + "SC-03J", + "SCV35", + "SM-G955F", + "SM-G955N", + "SM-G955W", + "SM-G9550", + "SM-G955U", + "SM-G955U1" + ], + "nfcPos": { + "x0": 0.15602836879432624, + "y0": 0.3054662379421222, + "x1": 0.8723404255319149, + "y1": 0.6334405144694534 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S8", + "modelNames": [ + "SC-02J", + "SCV36", + "SM-G950F", + "SM-G950N", + "SM-G950W", + "SM-G9500", + "SM-G9508", + "SM-G950U", + "SM-G950U1" + ], + "nfcPos": { + "x0": 0.1448275862068965, + "y0": 0.36538461538461536, + "x1": 0.8551724137931034, + "y1": 0.6955128205128205 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S9+", + "modelNames": [ + "SC-03K", + "SCV39", + "SM-G965F", + "SM-G965N", + "SM-G9650", + "SM-G965W", + "SM-G965U", + "SM-G965U1" + ], + "nfcPos": { + "x0": 0.11564625850340138, + "y0": 0.38782051282051283, + "x1": 0.8707482993197279, + "y1": 0.7275641025641025 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S9", + "modelNames": [ + "SC-02K", + "SCV38", + "SM-G960F", + "SM-G960N", + "SM-G9600", + "SM-G9608", + "SM-G960W", + "SM-G960U", + "SM-G960U1" + ], + "nfcPos": { + "x0": 0.12328767123287676, + "y0": 0.3557692307692308, + "x1": 0.863013698630137, + "y1": 0.6826923076923077 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Flip 5G", + "modelNames": [ + "SCG04", + "SM-F7070", + "SM-F707B", + "SM-F707N", + "SM-F707U", + "SM-F707U1", + "SM-F707W" + ], + "nfcPos": { + "x0": 0.12745098039215685, + "y0": 0.6365979381443299, + "x1": 0.8333333333333334, + "y1": 0.884020618556701 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Flip LTE", + "modelNames": [ + "SCV47", + "SM-F7000", + "SM-F700F", + "SM-F700N", + "SM-F700U", + "SM-F700U1", + "SM-F700W" + ], + "nfcPos": { + "x0": 0.19565217391304346, + "y0": 0.6806451612903226, + "x1": 0.8043478260869565, + "y1": 0.9 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Flip3 5G", + "modelNames": [ + "SC-54B", + "SCG12", + "SM-F7110", + "SM-F711B", + "SM-F711N", + "SM-F711U", + "SM-F711U1", + "SM-F711W" + ], + "nfcPos": { + "x0": 0.08088235294117652, + "y0": 0.5973154362416108, + "x1": 0.9264705882352942, + "y1": 0.912751677852349 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Flip4 5G", + "modelNames": [ + "SC-54C", + "SCG17", + "SM-F7210", + "SM-F721B", + "SM-F721C", + "SM-F721N", + "SM-F721U", + "SM-F721U1", + "SM-F721W" + ], + "nfcPos": { + "x0": 0.06617647058823528, + "y0": 0.5709677419354838, + "x1": 0.9264705882352942, + "y1": 0.8774193548387097 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Fold2 5G", + "modelNames": [ + "SM-F9160", + "SM-F916B", + "SM-F916N", + "SM-F916Q", + "SM-F916U", + "SM-F916U1", + "SM-F916W" + ], + "nfcPos": { + "x0": 0.12959381044487428, + "y0": 0.32319078947368424, + "x1": 0.9226305609284333, + "y1": 0.6077302631578947 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Fold3 5G", + "modelNames": [ + "SC-55B", + "SCG11", + "SM-F9260", + "SM-F926B", + "SM-F926N", + "SM-F926U", + "SM-F926U1", + "SM-F926W" + ], + "nfcPos": { + "x0": 0.10526315789473684, + "y0": 0.4129032258064516, + "x1": 0.9473684210526316, + "y1": 0.7516129032258064 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy Z Fold4 5G", + "modelNames": [ + "SC-55C", + "SCG16", + "SM-F9360", + "SM-F936B", + "SM-F936N", + "SM-F936U", + "SM-F936U1", + "SM-F936W" + ], + "nfcPos": { + "x0": 0.10447761194029848, + "y0": 0.45806451612903226, + "x1": 0.9328358208955224, + "y1": 0.7967741935483871 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel (2016)", + "modelNames": [ + "Pixel" + ], + "nfcPos": { + "x0": 0.5182481751824817, + "y0": 0.0, + "x1": 0.6496350364963503, + "y1": 0.011673151750972763 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 2 (2017)", + "modelNames": [ + "Pixel 2" + ], + "nfcPos": { + "x0": 0.33884297520661155, + "y0": 0.0622568093385214, + "x1": 0.487603305785124, + "y1": 0.13229571984435798 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 3 (2018)", + "modelNames": [ + "Pixel 3" + ], + "nfcPos": { + "x0": 0.17098445595854928, + "y0": 0.12224938875305623, + "x1": 0.7305699481865284, + "y1": 0.3863080684596577 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 3a (2019)", + "modelNames": [ + "Pixel 3a" + ], + "nfcPos": { + "x0": 0.2195121951219512, + "y0": 0.14788732394366197, + "x1": 0.7463414634146341, + "y1": 0.4014084507042254 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 4 (2019)", + "modelNames": [ + "Pixel 4" + ], + "nfcPos": { + "x0": 0.10188679245283017, + "y0": 0.15845070422535212, + "x1": 0.41132075471698115, + "y1": 0.3028169014084507 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 4a (2020)", + "modelNames": [ + "Pixel 4a" + ], + "nfcPos": { + "x0": 0.4957983193277311, + "y0": 0.39096267190569745, + "x1": 0.6428571428571428, + "y1": 0.45972495088408644 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 4a (5G)", + "modelNames": [ + "Pixel 4a (5G)" + ], + "nfcPos": { + "x0": 0.44339622641509435, + "y0": 0.3858093126385809, + "x1": 0.5849056603773585, + "y1": 0.4523281596452328 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 5", + "modelNames": [ + "Pixel 5" + ], + "nfcPos": { + "x0": 0.4416243654822335, + "y0": 0.34988179669030733, + "x1": 0.5939086294416244, + "y1": 0.42080378250591016 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 5a (5G)", + "modelNames": [ + "Pixel 5a" + ], + "nfcPos": { + "x0": 0.44339622641509435, + "y0": 0.3858093126385809, + "x1": 0.5849056603773585, + "y1": 0.4523281596452328 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 6 Pro", + "modelNames": [ + "Pixel 6 Pro" + ], + "nfcPos": { + "x0": 0.43434343434343436, + "y0": 0.5373831775700935, + "x1": 0.5808080808080809, + "y1": 0.6051401869158879 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 6", + "modelNames": [ + "Pixel 6" + ], + "nfcPos": { + "x0": 0.45744680851063835, + "y0": 0.4737903225806452, + "x1": 0.6117021276595744, + "y1": 0.532258064516129 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 6a", + "modelNames": [ + "Pixel 6a" + ], + "nfcPos": { + "x0": 0.45789473684210524, + "y0": 0.4778225806451613, + "x1": 0.6105263157894737, + "y1": 0.5362903225806451 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 7 Pro", + "modelNames": [ + "Pixel 7 Pro" + ], + "nfcPos": { + "x0": 0.4097222222222222, + "y0": 0.2602291325695581, + "x1": 0.5902777777777778, + "y1": 0.3453355155482815 + } + }, + { + "manufacturer": "Google", + "marketingName": "Pixel 7", + "modelNames": [ + "Pixel 7" + ], + "nfcPos": { + "x0": 0.40909090909090906, + "y0": 0.26143790849673204, + "x1": 0.6, + "y1": 0.35294117647058826 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy A34 5G", + "modelNames": [ + "SM-A3460", + "SM-A346B", + "SM-A346E", + "SM-A346M", + "SM-A346N" + ], + "nfcPos": { + "x0": 0.04054054054054057, + "y0": 0.016835016835016835, + "x1": 0.7297297297297297, + "y1": 0.27946127946127947 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23 Ultra", + "modelNames": [ + "SC-52D", + "SM-S9180", + "SM-S918B", + "SM-S918N", + "SM-S918U", + "SM-S918U1", + "SM-S918W" + ], + "nfcPos": { + "x0": 0.05405405405405406, + "y0": 0.3468013468013468, + "x1": 0.9324324324324325, + "y1": 0.8316498316498316 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23+", + "modelNames": [ + "SM-S9160", + "SM-S916B", + "SM-S916N", + "SM-S916U", + "SM-S916U1", + "SM-S916W" + ], + "nfcPos": { + "x0": 0.03355704697986572, + "y0": 0.33557046979865773, + "x1": 0.9463087248322147, + "y1": 0.7550335570469798 + } + }, + { + "manufacturer": "Samsung", + "marketingName": "Samsung Galaxy S23", + "modelNames": [ + "SC-51D", + "SM-S9110", + "SM-S911B", + "SM-S911C", + "SM-S911N", + "SM-S911U", + "SM-S911U1", + "SM-S911W" + ], + "nfcPos": { + "x0": 0.04054054054054057, + "y0": 0.34459459459459457, + "x1": 0.9459459459459459, + "y1": 0.7533783783783784 + } + } +] \ No newline at end of file diff --git a/android/src/main/res/values-ar/strings.xml b/app/features/src/main/res/values-ar/strings.xml similarity index 70% rename from android/src/main/res/values-ar/strings.xml rename to app/features/src/main/res/values-ar/strings.xml index bf0163a7..2eb3cdbe 100644 --- a/android/src/main/res/values-ar/strings.xml +++ b/app/features/src/main/res/values-ar/strings.xml @@ -17,18 +17,18 @@ تم مسح هذا الرمز للوصفة من قبل - تم التعرف على %s وصفة + تم التعرف على الوصفة %s - تم التعرف على %s وصفات + تم التعرف على وصفات %s إلغاء ضوء الكاميرا - إلغاء المسح الضوئي لكود الوصفة الطبية؟ - إلغاء المسح الضوئي - المواصلة - ابدأ الآن + إلغاء المسح الضوئي؟ + نعم + عدم الإلغاء + دعنا نذهب ما تحتاج إليه: أدخل رقم الوصول للبطاقة إدخال رقم التعريف الشخصي @@ -36,11 +36,11 @@ فشل الاتصال بالخادم. - لديك %s محاولة أخرى قبل وقف البطاقة. + لديك %s محاولة أخرى قبل أن يتم حظر بطاقتك. - لديك %s محاولات أخرى قبل وقف البطاقة. + لديك %s المحاولات الإضافية قبل أن يتم حظر بطاقتك. تجد رقم تسجيل الدخول أعلى يمينًا في بطاقتك الصحية. إلغاء @@ -74,8 +74,8 @@ فتح الموقع الإلكتروني أهلًا وسهلًا ابدأ تسجيل الدخول - الفتح - التسجيل + إلغاء الحظر + يسجل إلغاء الأمان تعليمات قانونية @@ -86,7 +86,7 @@ وضع علامة \"تم الصرف\" وضع علامة \"لم يتم الصرف\" شكل الجرعة - مقاس معياري + حجم العبوة الشخص المؤمن عليه الاسم العنوان @@ -103,7 +103,7 @@ العنوان رقم المُنْشَأَة رقم الهاتف - البريد الإلكتروني + عنوان البريد الإلكتروني حادث عمل يوم الحادث رقم شركة التأمين على الحوادث أو صاحب العمل @@ -113,7 +113,7 @@ أوقات العمل الموقع الإلكتروني قابلة للصرف اليوم فقط كدافع ذاتي - التسجيل + يسجل تفعيل وظيفة NFC يُرجى تفعيل وظيفة NFC بجهازك لتتمكن من تسجيل الدخول باستخدام بطاقتك الصحية. تفعيل @@ -129,10 +129,10 @@ الإعدادات منع أخذ لقطات الشاشة يمنع عرض الصور المصغرة للمعاينة عند التبديل بين التطبيقات - هل تسمح للوصفات الطبية الإلكترونية بتحليل سلوك الاستخدام دون إفصاح عن الهوية؟ + هل تسمح للوصفة الطبية الإلكترونية بتحليل سلوك الاستخدام الخاص بك بشكل مجهول؟ معلومات تقنية أمان بيانات وصفاتك - يُرجى الانتباه إلى أن الأشخاص الذين تشارك معهم هذا الجهاز والذين قد يمكنهم تخزين الصفات البيومترية لهم على هذا الجهاز أو الذين لديهم رقم تعريف شخصي للجهاز أو نمط مسح أو كلمة مرور، قد يمكنهم أيضًا الوصول إلى وصفاتك الطبية. + يُرجى الانتباه إلى أن الأشخاص الذين تشارك معهم هذا الجهاز والذين قد يمكنهم تخزين الصفات البيومترية لهم على هذا الجهاز، قد يمكنهم أيضًا الوصول إلى وصفاتك الطبية. فشل الإرسال لم يتم إعداد بريد إلكتروني بالبرنامج لا توجد نتائج @@ -145,28 +145,28 @@ أرغب في المساعدة في تحسين هذا التطبيق. ويتضمن ذلك معلومات عن الأجهزة والبرامج الموجودة على هاتفك، وإعدادات تطبيق الوصفات الطبية الإلكترونية وحجم الاستخدام، ولكنه لا يتضمن مطلقًا بيانات حول شخصك أو حالتك الصحية. وتُتاح البيانات فقط لشركة gematik GmbH بواسطة معالجي البيانات وتُحذف بعد 180 يومًا على أقصى تقدير. كما يمكنك إلغاء تفعيل التحليل في أي وقت من قائمة التطبيق. - تمكننا هذه البيانات من فهم وتحسين الوظائف التي تُستخدم بشكل متكرر. كما يمكننا أيضًا تقييم المدة التي يجب دعم التكنولوجيا الأقدم فيها ومتى يجب علينا مثلًا تحديث إصدار نظام التشغيل بشكل إلزامي دون التأثير على (عدد كبير جدًا) من المستخدمين. + تتيح لنا هذه البيانات فهم الوظائف المستخدمة بشكل متكرر وتحسينها. يمكننا أيضًا تقدير المدة التي تحتاج فيها التكنولوجيا القديمة إلى الدعم ومتى يمكننا، على سبيل المثال، جعل إصدار نظام التشغيل الأحدث إلزاميًا دون التأثير على (عدد كبير جدًا) من المستخدمين. تحسين التطبيق يظل التحليل دون إفصاح عن الهوية غير مفعل %s شكرًا لك على المساعدة! - التسجيل + يسجل يُرجى تحديد هويتك لتنزيل الوصفات. ملاحظة للصيدليات: يحصل هذا التطبيق على تفاصيل الاتصال والمعلومات حول الصيدليات من الموقعmein-apothekenportal.de التابع لاتحاد الصيدليات الألماني ج.م. هل اكتشفت خطأ أو ترغب في تصحيح البيانات؟ معرفة المزيد الصيدليات فشلك المحاولة للأسف \uD83D\uDE15 - الرجاء التجربة مرة أخرى - إدخال كلمة السر + من فضلك حاول مرة أخرى. + إدخال كلمة المرور متابعة وسائل المساعدة في الاستخدام التكبير يتيح تكبير حجم التطبيق عبر ضم أو سحب الأصابع (الشد للتكبير). - كلمة السر + كلمة المرور قم بتأمين بياناتك بكلمة سر من اختيارك. - كلمة السر + كلمة المرور حفظ إظهار كلمة السر - كرر كلمة السر + كرر كلمة المرور التوصيات:%s كتابة بريد إلكتروني أثناء إرسال رسالتك سيتم نقل المعلومات التالية عبر الجهاز ونظام التشغيل المُستخدم: @@ -174,22 +174,22 @@ لا يمكنك بعد إرسال الوصفات الطبية الإلكترونية إلى هذه الصيدلية. مفتوح حديثًا خدمة المراسلة - إرسال + الإرسال الفلتر الفرز لا يوجد مقر متاح مفهوم كلمة المرور المكرر مطابقة خطأ 20 10 76631 - شهادة بطاقتك الصحية غير صالحة. هل ربما انتهت صلاحية بطاقتك؟ يُرجى الاتصال بشركة التأمين الصحي الخاصة بك. + شهادة بطاقتك الصحية غير صالحة. ربما انتهت صلاحية بطاقتك؟ يرجى الاتصال بشركة التأمين الصحي الخاصة بك. محاولات تسجيل دخول غير ناجحة - تبين وجود عدد %s محاولة تسجيل خاطئة + تم اكتشاف %s محاولات تسجيل الدخول غير الناجحة. - تبين وجود عدد %s محاولات تسجيل خاطئة + تم اكتشاف %s محاولات تسجيل الدخول غير الناجحة. اختر الطريقة الأكثر أمانًا يمكن أن يكون هذا بصمة الإصبع أو نمط التمرير السريع أو ما شابه ذلك @@ -200,7 +200,7 @@ لا يتوفر الرمز الموحد المميز (SSO) تم النسخ إلى الحافظة اضغط لإضافة الرمز المميز إلى الحافظة - سارِ اليوم فقط + صالحة اليوم فقط السماح لا يوجد اتصال بالخادم يُرجى المحاولة من جديد بعد دقائق قليلة @@ -216,7 +216,7 @@ فشل الاتصال بالخادم: يُرجى التحقق من اتصال الإنترنت وإعدادات الوقت / التاريخ. تحذير قد يكون جهازك قد قلل من مستوى الأمان - يمكن أن يحدث هذا ، على سبيل المثال ، عن طريق الأجهزة التي تم التلاعب بها أو وضع المطور المنشط. لأسباب أمنية ، لا نوصي باستخدام التطبيق على أجهزة مكسورة الحماية. + يمكن أن يحدث هذا، على سبيل المثال، بسبب الأجهزة التي يتم التلاعب بها أو عند تشغيل وضع المطور. نوصي بعدم استخدام التطبيق على الأجهزة التي تم كسر الحماية فيها لأسباب أمنية. أنا على دراية بالمخاطر ومع ذلك أرغب في المواصلة. لماذا تعتبر الأجهزة التي تعمل بنظام التجذير أحد المخاطر الأمنية المحتملة؟ معرفة المزيد @@ -235,17 +235,17 @@ يوجد بروفايل بالفعل بنفس الاسم المذكور. البروفايل %s تم الاختيار - صورة الخلفية + لون الخلفية Frühlingsgrau Sonnentau Es! Ist! Rosa! Baum Blauer Mond September - لم يتم التسجيل + لم يتم تسجيل الدخول تم الاتصال كان آخر اتصال في %s حذف البروفايل؟ - سيؤدي هذا إلى حذف جميع بيانات البروفايل الموجودة بهذا الجهاز. ستبقى الوصفات الطبية الخاصة بك في الشبكة الصحية كما هي. + سيؤدي هذا إلى حذف جميع البيانات من الملف الشخصي على هذا الجهاز. سيتم الاحتفاظ بالوصفات الطبية الخاصة بك في الشبكة الصحية. حذف إلغاء حذف البروفايل @@ -264,26 +264,26 @@ لا توجد وصفات طبية جديدة - تم تحديث %s من الوصفة + وصفة %s الجديدة - تم تحديث %s من الوصفات + %s وصفات جديدة قابلة للصرف في الخلاص تم الصرف غير معروف عرض بروتوكول الدخول - يمكنك أن ترى هنا من وصل إلى وصفاتك الطبية - المقصود به هو مفتاح دخول لخدمة الوصفات الطبية + من الذي وصل إلى وصفاتك ومتى؟ + مفتاح الوصول إلى خدمة الوصفات الطبية بروتوكول الدخول لا يوجد بروتوكول للدخول لا يوجد بروتوكول للدخول بعد. جاري معالجة الوصفة الطبية في الوقت الحالي ولا يمكن صرفها. - الموافقة + استعرض - قبل - قبول يبدو أن المحاولة فشلت - نحن على علم بأن الربط بالبطاقة الصحية له عيوبه. لهذا فمن المقرر أن يكون التسجيل في المستقبل ممكنًا أيضًا عبر تطبيق تأمين صحي معتمد بالفعل.\n\nنحن نعمل أيضًا على تمكين صرف الوصفات الطبية رقميًا دون تسجيل.\n\nهل لاحظت أي شيء أثناء هذه العملية تود مشاركته معنا؟ يرجى الكتابة إلينا ، ويسعدنا أيضًا تلقي تعليقات نقدية للغاية. + ونحن ندرك أن الارتباط بالبطاقة الصحية له مخاطره. في المستقبل، يجب أن يكون التسجيل ممكنًا أيضًا عبر تطبيق التأمين الصحي المعتمد بالفعل. \n\n نحن نعمل أيضًا على ضمان إمكانية صرف الوصفات الطبية رقميًا دون التسجيل. \n\n هل لاحظت أي شيء خلال هذه العملية وترغب في مشاركته معنا؟ يرجى الكتابة إلينا، وسنكون سعداء أيضًا بتلقي تعليقات انتقادية للغاية. نصائح الاتصال حسن من قوة شبكة الاتصال انزع عند الضرورة علبة الحماية. @@ -313,11 +313,11 @@ حفظ الوصفات الطبية على الجهاز - المواصلة مع الوصفة %s + متابعة مع وصفة %s - المواصلة مع الوصفات %s + متابعة مع وصفات %s فضل الارتباط بالبطاقة الصحية بروفايلك الحالي مرتبط بالفعل ببطاقة صحية أخرى (رقم تأمين صحي %s). @@ -358,11 +358,11 @@ صالح حتى الصنف لقاح - الموافقة + استعرض - قبل - قبول تراجع ملاحظة ساعدنا في جعل هذا التطبيق أفضل - اختر كلمة مرور خاصة + إدخال كلمة المرور يجب أن تحتوي كلمة المرور على ثمانية حروف على الأقل قوة كلمة المرور غير كافية قوة كلمة المرور كافية @@ -381,7 +381,7 @@ أضف الوصفات إلى قائمتك عن طريق النقر على زر المسح في الزاوية اليمنى العليا. المسح الضوئي للنسخة الورقية يجب عليك تسجيل الدخول لتلقي الوصفات تلقائيا. - التسجيل + يسجل لا توجد وصفات طبية تم صرفها يتم هنا عرض الوصفات الطبية الخاصة بك التي تم صرفها. لأسباب تتعلق بحماية البيانات ، سيتم حذف وصفاتك من خادم الوصفات بعد 100 يوم. لا توجد وصفات طبية تم صرفها @@ -390,11 +390,11 @@ الأجهزة المتصلة مسجل منذ %s (هذا الجهاز) مسجل منذ %s - لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات الطبية بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. + لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. رمز PIN أدخل رقم التعريف الشخصي (البطاقة الصحية). متابعة - تسجيل الدخول + يسجل الأجهزة المتصلة حذف الجهاز؟ إلغاء @@ -417,28 +417,28 @@ لقد قمنا بجمع بعض النصائح لك لحل المشكلات الأكثر شيوعًا. ابدأ نصائح الاتصال إلغاء الحظر - تم حظر البطاقة + بطاقة محظورة تم إدخال رقم تعريف شخصي خاطيء ثلاث مرات. لذلك تم حظر بطاقتك لأسباب أمنية. - إلغاء حظر البطاقة - أدخل رمز PUK - لقد تلقيت مع رقم التعريف الشخصي الخاص بك رمز PUK مكون من 8 حروف في خطاب من شركة التأمين الصحي الخاصة بك. + فتح البطاقة + أدخل PUK + باستخدام رقم التعريف الشخصي ، تلقيت رمز PUK المكون من 8 أرقام من شركة التأمين الخاصة بك. اختيار رقم PIN جديد يمكنك اختيار رقم التعريف الشخصي الجديد (PIN) بنفسك (من 6 إلى 8 أرقام). - سجلت رقم التعريف الشخصي؟ + تذكرت PIN؟ يرجى تدوين رقم التعريف الشخصي الخاص بك والاحتفاظ به في مكان آمن. إلغاء موافق لا يمكن إلغاء الحظر لقد وصلك باستخدام مفتاح فك القفل الشخصي إلى العدد الأقصى لعمليات إلغاء الحظر بالبطاقات أو أدخلته بشكل خاطيء مرات متكررة. يُرجى التوجه إلى شركة التأمين الخاصة بك. - يمكنك استخدام مفتاح فك القفل الشخصي حتى 10 محاولات إلغاء الحظر. - تم إلغاء حظر البطاقة + يمكنك استخدام رمز PUK واحد لما يصل إلى 10 عمليات فتح. + بطاقة مقفلة ما تحتاج إليه: بطاقتك الصحية مفتاح فك القفل الشخصي لبطاقتك الصحية متابعة - البطاقة الصحية + بطاقة صحية اطلب رقم التعريف الشخصي أو البطاقة - التسجيل + يسجل كيف تريد تسجيل الدخول؟ بطاقة صحية مزودة بتقنية الإتصال اللاسلكية رقم التعريف الشخصي للبطاقة الصحية @@ -450,11 +450,11 @@ لا تحتوي بطاقتي على رقم الدخول - لديك عدد %s محاولة إضافية قبل حظر بطاقتك. + لديك %s محاولة أخرى قبل أن يتم حظر بطاقتك. - لديك عدد %s محاولات إضافية قبل حظر بطاقتك. + لديك %s المحاولات الإضافية قبل أن يتم حظر بطاقتك. ضع بطاقتك الصحية على ظهر الهاتف يمكن أن تستمر العملية التالية حتى 30 ثانية. @@ -476,7 +476,7 @@ لم يعد صالحًا التسجيل بالتطبيق اختر التأمين الصحي - لم تجد ما تبحث عنه؟ يتم زيادة هذه القائمة باستمرار. يتم في الوقت الحالي دعم التسجيل بالبطاقة الصحية لكل شركة تأمين صحي. + لم تجد ما كنت تبحث عنه؟ يتم توسيع هذه القائمة باستمرار. التسجيل بالبطاقة الصحية مدعوم بالفعل من قبل كل شركة تأمين صحي. ملاحظة من تطبيق الوصفة الإلكترونية نحن سعداء بملاحظاتك. يرجى استخدام المساحة أدناه مع توخي الدقة قدر الإمكان: مفتاح فك القفل الشخصي @@ -488,105 +488,105 @@ حفظ عدم الحفظ ملاحظة - لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات الطبية بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. + لأسباب أمنية ، يتم إنهاء الاتصال بخادم الوصفات بعد 12 ساعة. لإعادة الاتصال ، تحتاج إلى بطاقة صحية ورقم تعريف شخصي لكل عملية اتصال. إعداد التأمين البيومتري لا يمكن حفظ بيانات الدخول. قم بإعداد التأمين البيومتري (مثل بصمة الإصبع) على جهازك مسبقًا. إلغاء الإعدادات ملاحظة - الموافقة + استعرض - قبل - قبول أمان بيانات وصفاتك \ \"يستخدم هذا التطبيق المستشعر البيومتري الأكثر أمانًا الذي يوفره جهازك لتخزين بيانات الاعتماد الخاصة بك في منطقة آمنة من ذاكرة الجهاز. \\" يسمح لكنظام التأمين البيومتري لبيانات الدخول الخاصة بك بفتح هذا التطبيق في المستقبل دون الحاجة إلى إدخال رقم التعريف الشخصي أو البطاقة الصحية ، وعرض الوصفات الطبية أو استدعائها أو استرداد قيمتها أو حذفها. - يُرجى الانتباه إلى أن الأشخاص الذين تشارك معهم هذا الجهاز والذين قد يمكنهم تخزين الصفات البيومترية لهم على هذا الجهاز أو الذين لديهم رقم تعريف شخصي للجهاز أو نمط مسح أو كلمة مرور، قد يمكنهم أيضًا الوصول إلى وصفاتك الطبية. + يُرجى الانتباه إلى أن الأشخاص الذين تشارك معهم هذا الجهاز والذين قد يمكنهم تخزين الصفات البيومترية لهم على هذا الجهاز، قد يمكنهم أيضًا الوصول إلى وصفاتك الطبية. فشلت المحاولة للأسف - لم تنجح المصادقة باستخدام باستخدام تطبيق صندوق التأمين الصحي. - انتهت الصلاحية في %s - تم بالفعل حذف الوصفة من الخادم - يرجى تصحيح المدخلات الخاصة بك أو تجاهل التغييرات + لم تكن المصادقة باستخدام تطبيق التأمين الصحي ناجحة. + انتهت صلاحيته في %s + لقد تم بالفعل حذف الوصفة من الخادم + الرجاء تصحيح إدخالك أو تجاهل التغييرات التصويب البيانات المؤمن عليه الاسم التأمين الصحي الرقم التأميني رقم الوصول للبطاقة - التسجيل + يسجل تسجيل الخروج حفظ - يتغيرون + يتغير تعديل الصورة الشخصية متابعة الخادم لا يستجيب الرجاء معاودة المحاولة في وقت لاحق. حاول مرة أخرى - ابحث عن التأمين - هل تريد الاتصال بخادم الوصفات الآن؟ + البحث عن التأمين + الاتصال بخادم الوصفة الآن؟ تم تسجيل الدخول بنجاح - فقد الاتصال - هل تريد الاتصال بخادم الوصفات الآن؟ - لا توجد رموز - ستتلقى رمزًا مميزًا عند تسجيل الدخول إلى خدمة الوصفات الطبية.\n + فقدت الاتصال + الاتصال بخادم الوصفة الآن؟ + لا الرموز + سوف تتلقى رمزًا مميزًا عند تسجيل الدخول إلى خدمة الوصفات الطبية.\n الطلب #٪ s حدد رقم التعريف الشخصي المطلوب - إلغاء حظر البطاقة - اختر PIN - كرر PIN + فتح البطاقة + حدد رقم التعريف الشخصي + كرر رقم التعريف الشخصي الإدخالات تختلف عن بعضها البعض. - لا توجد أوامر - ليس لديك أي طلبات حتى الآن. + لا أوامر + ليس لديك أي أوامر حتى الآن. الآن - الساعة %s + في الساعة %s عربة التسوق جاهزة - تمت إضافة الوصفة إلى عربة التسوق الخاصة بك. يرجى زيارة موقع الصيدلية لإتمام الطلب. + تمت إضافة الوصفة إلى سلة التسوق الخاصة بك. برجاء الدخول إلى موقع الصيدلية لإتمام الطلب. فتح عربة التسوق - أظهر رمز الاستلام هذا في الصيدلية. + أظهر رمز التجميع هذا في الصيدلية. الحصول على كود الاستلام لا يمكن عرض الرسالة - الرجاء الاتصال بالصيدلية الخاصة بك ( %s ). - عرض رابط عربة التسوق + من فضلك اتصل بالصيدلية الخاصة بك ( %s ). + إظهار رابط سلة التسوق عرض كود الاستلام - اعرض الرسالة - %s الساعة %s + إظهار الرسالة + %s في الساعة %s تم إرسال الوصفة إلى %s . نظرة عامة على الطلب جديد - مسار - أمر - مجاني للمتصل. أوقات الخدمة: الاثنين - الجمعة 8:00 صباحًا - 8:00 مساءً باستثناء أيام العطلات الوطنية + دورة + الأمر - الطلب + مجانا للمتصل. أوقات الخدمة: الإثنين - الجمعة 8:00 صباحًا - 8:00 مساءً باستثناء أيام العطل الوطنية الصيدلية حدد رقم التعريف الشخصي المطلوب تم حفظ رقم التعريف الشخصي المطلوب - حاليا مفتوحة وقريبة مني + مفتوحة حاليا وقريبة مني مصنف بواسطة … ابدأ البحث - الاحالة المباشرة + التكليف المباشر الصيدليات - رقم الهاتف (اختيارى) + رقم الهاتف (اختياري) البحث بالاسم أو العنوان لا توجد معلومات صيدلية صالحة - لم يتم العثور على معلومات حالية حول هذه الصيدلية. سيتم حذف الإدخال الخاص بهذه الصيدلية. + لم يتم العثور على معلومات حالية عن هذه الصيدلية. سيتم حذف الإدخال الخاص بهذه الصيدلية. موافق - دليل الصيدلة غير متاح - حاليا لا يمكن استرجاع أي معلومات حالية عن هذه الصيدلية. الرجاء التحقق من اتصال الانترنت الخاص بك. + دليل الصيدلية غير متوفر + حاليا لا يمكن الوصول إلى أي معلومات حالية حول هذه الصيدلية. الرجاء التحقق من اتصال الانترنت الخاص بك. إلغاء حاول مرة أخرى حفظ البيئة تسجيل الدخول غير ممكن - يبدو أن بيانات اعتماد تسجيل الدخول الخاصة بك قد تغيرت. يرجى التسجيل مرة أخرى ببطاقتك الصحية. + يبدو أن خصائص تسجيل الدخول البيومترية الخاصة بك قد تغيرت. الرجاء تسجيل الدخول مرة أخرى باستخدام بطاقتك الصحية. إلغاء - التسجيل + يسجل الملف الشخصي 1 قريب مني - يمكن استردادها لاحقًا - قابل للاسترداد من %s + قابلة للاسترداد في وقت لاحق + يمكن الاسترداد من %s تحسينات المنتج تحليل مجهول - ساعدنا في جعل هذا التطبيق أفضل. يتم جمع جميع بيانات المستخدم بشكل مجهول ويتم استخدامها فقط لتحسين تجربة المستخدم. - أمن الجهاز + ساعدنا في جعل هذا التطبيق أفضل. يتم جمع كافة بيانات الاستخدام بشكل مجهول ويتم استخدامها حصريًا لتحسين تجربة المستخدم. + أمان الجهاز اعدادات شخصية وسائل المساعدة في الاستخدام تحسينات المنتج - الوصفة المضافة + وصفة المضافة الوصفة متاحة بالفعل حدث خطأ أثناء الاستيراد حذف @@ -595,11 +595,11 @@ نسيت رقم التعريف الشخصي - روشتة + %s الوصفة - الوصفات الطبية + %s وصفات لقد قرأت وقبلت سياسة الخصوصية وشروط الاستخدام. سياسة الخصوصية @@ -610,60 +610,59 @@ يتم بالطبع جمع جميع البيانات بشكل مجهول. يمكنك تغيير هذا القرار في أي وقت في إعدادات النظام. المواصلة - الموافقة + استعرض - قبل - قبول يستخدم هذا التطبيق الطريقة الأكثر أمانًا التي يوفرها جهازك. حفظ يختار الدواء اسم تجاري نعم - رقم - جرعة + لا + الجرعة تاريخ المسألة سيتم استبدال هذه الوصفة لك كجزء من العلاج. بدون بيان دفع اضافي الدواء - مذكرات التسليم + تعليمات التقديم مؤهلة وفقًا لـ BVG - تحضير بديل - اسم الصيغة + إعداد بديل + اسم الوصفة التعبئة والتغليف - تعليمات صياغة + تعليمات التصنيع وصف معطى بواسطة الصادر في: المادة الفعالة المنصوص عليها استلام - ما هو التعيين المباشر؟ - في حالة الإحالات المباشرة ، يتم استرداد وصفة طبية من عيادة أو مستشفى مباشرة في الصيدلية. لا يتعين على الأشخاص المؤمن عليهم اتخاذ أي إجراء ولا يمكنهم التدخل في عملية الاسترداد. \n\n يتم سرد الإحالات المباشرة في تطبيق الوصفات الطبية الإلكترونية لجعل علاجك أكثر شفافية بالنسبة لك. + ما هو التكليف المباشر؟ + مع الإحالة المباشرة، يتم صرف الوصفة الطبية من عيادة أو مستشفى مباشرة في الصيدلية. لا يتعين على الأشخاص المؤمن عليهم اتخاذ أي إجراء ولا يمكنهم التدخل في عملية الاسترداد. \n\n يتم إدراج الإحالات المباشرة في تطبيق الوصفات الطبية الإلكترونية لجعل علاجك أكثر شفافية بالنسبة لك. رسوم خدمة الطوارئ في بعض الأحيان هناك حاجة للتسرع. يمكن استبدال بعض الوصفات الطبية دون دفع رسوم خدمة الطوارئ الإضافية ، مثل الليل أو في أيام العطلات. الأدوية الخاضعة للدفع المشترك - معفى من المشاركة - يجب على أولئك الذين لديهم تأمين صحي قانوني دفع مبلغ مشترك يصل إلى عشرة يورو للأدوية الموصوفة. \n\n يعتمد مبلغ الدفع المشترك على سعر الدواء الخاص بك. عليك أن تدفع ثمن الأدوية التي تقل تكلفتها عن 5 يورو بنفسك.\n بالنسبة للأدوية الأكثر تكلفة ، عليك أن تدفع عشرة بالمائة من السعر ، ولكن على الأقل 5 يورو و 10 يورو كحد أقصى. \n\n يُعفى الأطفال والشباب الذين تقل أعمارهم عن 18 عامًا بشكل عام من المشاركة في الدفع. \n\n إذا تجاوزت تكاليف الأدوية السنوية الخاصة بك الحد المالي الخاص بك ، فيمكن إعفائك من الدفع المشترك. تحدث إلى شركة التأمين الصحي الخاصة بك حول هذا الموضوع. - أنت معفى من الدفع المشترك لهذا الدواء. سيغطي تأمينك الصحي تكلفة الدواء. - ما هي مدة صلاحية هذه الوصفة؟ + معفاة من الدفع الإضافي + يجب على أولئك الذين لديهم تأمين صحي قانوني دفع مبلغ إضافي يصل إلى عشرة يورو مقابل الأدوية الموصوفة. \n\n يعتمد مبلغ الدفعة الإضافية على سعر الدواء الخاص بك. يتعين عليك أن تدفع بنفسك ثمن الأدوية التي تكلف أقل من 5 يورو.\n بالنسبة للأدوية الأكثر تكلفة، عليك أن تدفع عشرة بالمائة من السعر، ولكن على الأقل 5 يورو والحد الأقصى 10 يورو. \n\n يُعفى الأطفال والشباب الذين تقل أعمارهم عن 18 عامًا بشكل عام من الدفع الإضافي. \n\n إذا تجاوزت تكاليف الدواء السنوية الحد الأقصى للعبء المالي، فيمكنك إعفاءك من الدفع المشترك. تحدث إلى شركة التأمين الصحي الخاصة بك حول هذا الموضوع. + أنت معفى من دفع دفعة مشتركة لهذا الدواء. سوف تقوم شركة التأمين الصحي الخاصة بك بتغطية تكلفة الدواء. + ما هي مدة صلاحية هذه الوصفة الطبية؟ خلال هذه الفترة، يمكنك استرداد الوصفة الطبية الخاصة بك في أي صيدلية بحد أقصى للدفع الإضافي قدره 10 يورو. يمكن تلقي مستحضرًا طبيًا بديلًا - نظرًا للمتطلبات القانونية لشركة التأمين الصحي الخاصة بك ، يمكن أن تحصل على بديل يحتوي على نفس العنصر النشط. \n\n يمكن أن تبدو الأدوية وتسمى بشكل مختلف ، ولها أسعار ومصنعون مختلفون ، لكنها لا تزال تحتوي على نفس العنصر النشط. العنصر النشط نفسه والجرعة مهمان بشكل خاص لتأثير الأدوية في الجسم. غالبًا ما يحصل المرضى في الصيدلية على دواء مختلف عن الدواء الموصوف من قبل الطبيب في الوصفة الطبية - بشرط أن تكون الأدوية متشابهة. يمكن أن تكون هناك أسباب علاجية واقتصادية للتغيير. + نظرًا للمتطلبات القانونية من شركة التأمين الصحي الخاصة بك، قد يتم إعطاؤك بديلاً بنفس العنصر النشط. \n\n يمكن أن تبدو الأدوية وتسميتها مختلفة، ولها أسعار ومصنعون مختلفون، ولكنها لا تزال تحتوي على نفس العنصر النشط. العنصر النشط نفسه والجرعة أمران حاسمان لتأثير الأدوية في الجسم. غالبًا ما يتلقى المرضى في الصيدلية دواءً مختلفًا عن الدواء الذي وصفه الطبيب - بشرط أن يكون الدواء مشابهًا. وقد تكون هناك أسباب علاجية واقتصادية للتغيير. الوصفة الطبية التي تم مسحها - لأسباب أمنية ، يجب ألا تعرض الوصفات الطبية المستوردة من المطبوعات الورقية أي بيانات شخصية أو طبية. \n\n سجّل الدخول إلى هذا التطبيق باستخدام البطاقة الصحية أو تطبيق التأمين لعرض جميع المعلومات الواردة في الوصفة الطبية. - الوصفة الطبية غير صحيحة + الوصفات الطبية المستوردة من نسخة مطبوعة لا يمكنها عرض المعلومات الشخصية أو الطبية لأسباب أمنية. \n\n قم بتسجيل الدخول إلى هذا التطبيق باستخدام البطاقة الصحية أو تطبيق التأمين لعرض جميع المعلومات الواردة في الوصفة الطبية. + الوصفة غير صحيحة تم إصدار هذه الوصفة بشكل غير صحيح. - الوصفة الطبية التي تم مسحها رسوم خدمة الطوارئ - الجرعة حسب التعليمات المكتوبة + الجرعة وفقا للتعليمات المكتوبة الهاتف - موقع + موقع إلكتروني البريد الإلكتروني الفرز حسب المسافة غير ممكن. موافق أدخل رقم التعريف الشخصي الحالي - تم إدخال رمز PIN غير صحيح - رقم التعريف الشخصي الحالي لبطاقتك الصحية - تم حظر البطاقة + تم إدخال رقم تعريف شخصي خاطئ + رقم التعريف الشخصي (PIN) الحالي لبطاقتك الصحية + بطاقة محظورة قم بإلغاء حظر بطاقتك في الإعدادات > إلغاء حظر البطاقة. لأسباب أمنية ، يرجى إدخال رقم التعريف الشخصي الحالي الخاص بك. نسيت رقم التعريف الشخصي @@ -671,159 +670,159 @@ الدواء يبدو أن شيئًا ما قد حدث خطأ أثناء إعداد وصفتك. هل تريد الإبلاغ عن خطأ؟ الإبلاغ - لم يتم التسجيل + لم يتم تسجيل الدخول مسجل مع - البطاقة الصحية + بطاقة صحية القياس البيومتري - لم يتم التسجيل - ونحن مهتمون في رأيك. يرجى تخصيص خمس دقائق للإجابة على استبياننا. شكرا لكم مقدما. + لم يتم تسجيل الدخول + ونحن مهتمون في رأيك. من فضلك خذ خمس دقائق لإكمال الاستبيان الخاص بنا. شكرا جزيلا لك مقدما. إشعار تحذير - تمت إضافة الصيدلة إلى المفضلة + تم اضافة الصيدلية الى المفضلة تمت إزالة الصيدلية من المفضلة صيدلياتي قوة كلمة المرور جيدة جدا عملية الكتابة غير ناجحة - تعذر حفظ رقم التعريف الشخصي + لا يمكن حفظ رقم التعريف الشخصي الإبلاغ - قم بتعيين PIN - انتهاك قاعدة الوصول - ليس لديك إذن للوصول إلى دليل الخرائط. - قم بتعيين رقم التعريف الشخصي الخاص بك - البطاقة مؤمنة برقم PIN من شركة التأمين الصحي الخاصة بك (رقم التعريف الشخصي للنقل) ، يرجى تعيين رقم التعريف الشخصي الخاص بك. + تعيين رقم التعريف الشخصي + تم انتهاك قاعدة الوصول + ليس لديك إذن للوصول إلى دليل الخريطة. + قم بتعيين الدبوس الخاص بك + البطاقة مؤمنة برقم سري من شركة التأمين الصحي الخاصة بك (رقم التعريف الشخصي للنقل)، يرجى إدخال رقم التعريف الشخصي الخاص بك. كلمة المرور لم يتم العثور لا توجد كلمة مرور مخزنة على بطاقتك. لقد تم تسجيل الخروج - قم بتسجيل الدخول مرة أخرى لتحديث الوصفات الطبية الخاصة بك. + قم بتسجيل الدخول مرة أخرى لتحديث وصفاتك. رقم العنصر النشط الفاعلية والوحدة - تم استرداد القيمة قبل %s دقيقة - استرداد في %s - استردت للتو - استرداد في الساعة %s + تم استردادها منذ %s دقيقة + تم الاسترداد بتاريخ %s + تم الاسترداد الآن + تم الاسترداد في الساعة %s ظهرًا الطلب #٪ s تم استبدال هذه الوصفة لك كجزء من العلاج. رسوم خدمة الطوارئ - لا يمكن ملء هذه الوصفة في الليل في الصيدلية دون دفع رسوم خدمة الطوارئ الإضافية. + لا يمكن صرف هذه الوصفة الطبية في الصيدلية ليلاً دون دفع رسوم خدمة الطوارئ الإضافية. ابحث هنا الإعدادات مشاركة الموقع في الإعدادات. قريب مني - عقد لتحرير الاسم. - أدخل الاسم الجديد لملف التعريف. - يجب عليك تسجيل الدخول لتلقي الوصفات الطبية الرقمية من عيادتك. - تلقي الوصفات الطبية رقميًا؟ - اسحب الشاشة لأسفل للتحديث. + اضغط مع الاستمرار لتحرير الاسم. + أدخل الاسم الجديد للملف الشخصي. + لتلقي الوصفات الطبية رقميًا من عيادتك، يجب عليك تسجيل الدخول. + تلقي الوصفات الطبية رقميا؟ + اسحب الشاشة للأسفل للتحديث. لا توجد وصفات طبية - أضف الوصفات الطبية باستخدام زر + في الزاوية اليمنى العليا. - تسجيل الدخول - أرشيف الوصفات الطبية + أضف وصفات باستخدام الزر + في الزاوية اليمنى العليا. + يسجل + أرشيف الوصفة ربما لاحقا - تسجيل الدخول + يسجل تعديل الصورة الشخصية - أرشيف الوصفات الطبية + أرشيف الوصفة أدخل الاسم حفظ طلبي - المستلم: في + المتلقي: في الوصفات الطبية الصيدلية - إرسال - للتغيير - التقط في الصيدلية + يرسل + يتغير + التقاط في الصيدلية التسليم عن طريق البريد - التسليم عن طريق البريد - وصفات %s - الاسترداد غير ممكن + التسليم عن طريق الطلب البريدي + %s وصفات + لا يمكن استردادها لا يمكن استبدال وصفة طبية واحدة أو أكثر. - لم يتم اختيار وصفة طبية - لاسترداد الوصفات الطبية ، يجب اختيار وصفة طبية واحدة على الأقل. - أضف معلومات الاتصال - للتغيير - بدون وصفة طبية + لم يتم اختيار أي وصفة + لاسترداد الوصفات، يجب تحديد وصفة واحدة على الأقل. + أضف تفاصيل الاتصال + يتغير + لا وصفة ليس لديك حاليا أي وصفات طبية قابلة للاسترداد - يلتقط - ساعي - إرسال - اختر الوصفات الطبية - انقر هنا لمسح الوصفات الطبية + مجموعة + فتى التوصيل + الإرسال + اختر الوصفات + انقر هنا لمسح الوصفات اضغط مع الاستمرار لتعديل الأسماء أضف المزيد من الملفات الشخصية ، على سبيل المثال لأطفالك أو والديك - انقر فوق الشاشة لتخطي تلميح الأداة المعروض. + انقر على الشاشة لتخطي تلميح الأداة الذي يظهر. كيفية تخليص؟ - كيف تريد أن تتلقى أدويتك؟ + كيف تريد أن تتلقى الدواء الخاص بك؟ تخليص مباشرة استبدال الدواء في الموقع اطلب حجز أو تسليمها تم - كود جماعي - رموز مفردة + كود الجمع + رموز فردية - لديك وصفة طبية %s . + لديك وصفة %s . - لديك %s وصفات طبية. + لديك وصفات %s . اختر اختيارًا - جميع الوصفات الطبية - ما هي الوصفات الطبية؟ + جميع الوصفات + أي وصفات؟ متابعة متابعة معرفة المزيد ملاحظة يستخدم هذا التطبيق برنامجًا من Google للتعرف على الرموز. معرفة المزيد - حول الماسح الضوئي رمز وصفة طبية + معلومات حول الماسح الضوئي لرمز الوصفة ما هي البيانات التي يحتوي عليها رمز الوصفة؟ - يحتوي رمز الوصفة الطبية فقط على معرف للوصفة الطبية. يسمح ذلك بإيجاد الوصفة الطبية في خدمة الوصفات الطبية في شبكة الصحة الرقمية. لا يحتوي رمز الوصفة الطبية على أي بيانات عنك أو عن أدويتك. - إذن لا أحد يستطيع فعل أي شيء بقانون الوصفة وحده؟ - صيح. يجب تنزيل بيانات الوصفات الطبية من خدمة الوصفات الطبية. هذا يتطلب تسجيل دخول آمن. + يحتوي رمز الوصفة فقط على معرّف للوصفة. وهذا يعني أنه يمكن العثور على الوصفة الطبية من خلال خدمة الوصفات الطبية في الشبكة الصحية الرقمية. لا يحتوي رمز الوصفة الطبية على أي معلومات عنك أو عن أدويتك. + إذن لا يمكن لأحد أن يفعل أي شيء باستخدام رمز الوصفة وحده؟ + صحيح. يجب تنزيل بيانات الوصفة الطبية من خدمة الوصفة الطبية. مطلوب تسجيل دخول آمن لهذا الغرض. من يمكنه التسجيل في خدمة الوصفات الطبية؟ - التسجيل في خدمة الوصفات الطبية في الشبكة الصحية الرقمية ممكن للأشخاص المؤمن عليهم والصيدليات والممارسات الطبية والمستشفيات. - لماذا يستخدم تطبيق الوصفات الطبية الإلكترونية ميزات Google؟ - تقدم Google وظائف يمكن دمجها بسهولة في التطبيقات والتي يتم تطويرها وتحديثها باستمرار بواسطة Google. هذا يضمن أن الوظائف تعمل على العديد من الأجهزة الطرفية المختلفة ويمكن تشغيلها بأمان. يستخدم التطبيق ميزة لتحسين وظائف الكاميرا والمسح الضوئي لأجهزة Android (Google ML Kit). - كيف يعمل تحسين Google ML Kit Scan؟ - تساعد Google ML Kit على تحسين الصورة الملتقطة بواسطة الكاميرا بحيث يمكن قراءة أكواد الوصفات الطبية حتى في ظروف الإضاءة السيئة أو مع طرز الكاميرا القديمة. - هل سيتم نقل البيانات المتعلقة بالوصفة الطبية أو الأدوية الخاصة بي إلى Google؟ - يتم حفظ رمز الوصفة الطبية المقروء مباشرةً في التطبيق ، ولن يتم تمريره إلى Google. لا يتم تخزين بيانات الوصفات الطبية في الكود ، فقط في شبكة الصحة الرقمية. من هناك يتم إرسالها إلى التطبيق. جوجل ليس لديها حق الوصول إلى شبكة الصحة الرقمية. + يمكن التسجيل في خدمة الوصفات الطبية في الشبكة الصحية الرقمية للأشخاص المؤمن عليهم والصيدليات والممارسات والمستشفيات. + لماذا يستخدم تطبيق الوصفات الطبية الإلكترونية ميزات جوجل؟ + توفر Google وظائف يمكن دمجها بسهولة في التطبيقات والتي تعمل Google على تطويرها وتحديثها باستمرار. وهذا يضمن أن الوظائف تعمل على العديد من الأجهزة المختلفة ويمكن تشغيلها بأمان. يستخدم التطبيق ميزة لتحسين وظائف الكاميرا والمسح الضوئي لأجهزة Android (Google ML Kit). + كيف يعمل تحسين المسح الضوئي مع Google ML Kit؟ + تساعد Google ML Kit على تحسين الصورة التي تلتقطها الكاميرا بحيث يمكن قراءة رموز الوصفات حتى في ظروف الإضاءة السيئة أو مع نماذج الكاميرا القديمة. + هل ستتم مشاركة البيانات المتعلقة بالوصفة الطبية أو الدواء مع Google؟ + لا. يتم حفظ قراءة رمز الوصفة مباشرة في التطبيق. ولن تتم مشاركته مع Google. لا يتم تخزين بيانات الوصفة الطبية في الكود، ولكن فقط في الشبكة الصحية الرقمية. ومن هناك يتم نقلهم إلى التطبيق. ليس لدى Google إمكانية الوصول إلى شبكة الصحة الرقمية. ما البيانات التي تعالجها Google عند استخدام ML Kit؟ - تمتلك Google فقط إمكانية الوصول إلى المعلومات الفنية حول الجهاز النهائي المستخدم والاستخدام العام للوظيفة الإضافية (مثل معدل الخطأ وإعدادات الكاميرا) من أجل تسجيل هذا إحصائيًا وبالتالي تحسين الوظيفة الإضافية. عند الوصول ، تسجل Google عنوان IP الخاص بجهازك الطرفي مؤقتًا. لن تسجل Google معلومات عنك ومحتويات الوصفة الطبية. - هل استخدام Google ML Kit تطوعي؟ - نعم ومع ذلك ، تم تضمين ML Kit في الماسح الضوئي لرمز الوصفة الطبية في إصدار Android من تطبيق الوصفات الطبية الإلكترونية. إذا كنت تستخدم الماسح الضوئي لرمز الوصفة الطبية على جهاز Android ، فسيتم أيضًا استخدام وظيفة ML Kit دائمًا. ومع ذلك ، يمكنك الاستغناء عن استخدام الماسح الضوئي للرموز الطبية. يمكن أيضًا تحميل الوصفات الطبية الخاصة بك في التطبيق إذا قمت بالتسجيل في الشبكة الصحية الرقمية باستخدام البطاقة الصحية الإلكترونية أو عبر تطبيق التأمين الصحي الخاص بك. - هل يمكنني رؤية من شاهد الوصفات الطبية الخاصة بي؟ - نعم. يتم تسجيل الدخول إلى بياناتك بالكامل في شبكة الصحة الرقمية. في تطبيق الوصفات الطبية الإلكترونية ، يمكنك معرفة من وصل إلى بياناتك. - بمن يمكنني الاتصال إذا كانت لدي أسئلة حول التطبيق أو الوصفة الإلكترونية؟ - يمكنك العثور على معلومات مفصلة في إعلان حماية البيانات. + تحصل Google فقط على حق الوصول إلى المعلومات الفنية حول الجهاز المستخدم والاستخدام العام للوظيفة الإضافية (مثل معدل الخطأ وإعدادات الكاميرا) لتسجيل ذلك إحصائيًا وبالتالي تحسين الوظيفة الإضافية. عند الوصول، يقوم Google بتسجيل عنوان IP الخاص بجهازك مؤقتًا. لا يتم تسجيل المعلومات المتعلقة بك ومحتويات الوصفة بواسطة Google. + هل استخدام Google ML Kit طوعي؟ + نعم. ومع ذلك، فإن ML Kit مضمن في الماسح الضوئي لرمز الوصفة في إصدار Android من تطبيق الوصفات الطبية الإلكترونية. إذا كنت تستخدم الماسح الضوئي لرمز الوصفة على جهاز Android، فسيتم استخدام وظيفة ML Kit دائمًا. ومع ذلك، يمكنك تجنب استخدام الماسح الضوئي لرمز الوصفة. يمكن أيضًا تحميل الوصفات الطبية الخاصة بك في التطبيق إذا قمت بتسجيل الدخول إلى شبكة الصحة الرقمية باستخدام البطاقة الصحية الإلكترونية أو عبر تطبيق التأمين الصحي الخاص بك. + هل يمكنني معرفة من شاهد وصفاتي؟ + نعم. يتم تسجيل كل إمكانية الوصول إلى بياناتك بالكامل في شبكة الصحة الرقمية. في تطبيق الوصفة الطبية الإلكترونية، يمكنك معرفة من قام بالوصول إلى بياناتك. + أين يمكنني الاتصال إذا كانت لدي أسئلة حول التطبيق أو الوصفة الطبية الإلكترونية؟ + يمكن العثور على معلومات مفصلة في إعلان حماية البيانات. عدد العبوات المقررة لا توجد وصفات طبية لهذا تحتاج إلى وصفات طبية قابلة للاسترداد. اختر التأمين الصحي - ابحث عن التأمين + البحث عن التأمين إلغاء ما الذي ترغب في طلبه؟ - بالنسبة لهذا التطبيق ، تحتاج إلى بطاقة ورقم التعريف الشخصي المرتبط بها. + لهذا التطبيق، تحتاج إلى بطاقة ورقم التعريف الشخصي المقابل. كيف تريد الاتصال بشركة التأمين الخاصة بك؟ تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية - تقدم شركة التأمين الخاصة بك خيارات الاتصال التالية + تقدم شركة التأمين الخاصة بك خيار الاتصال التالي إغلاق تم إدخال رقم التعريف الشخصي بشكل غير صحيح. تم إدخال رقم الوصول بشكل غير صحيح - تم إدخال PUK بشكل غير صحيح. - إيصالات المصروفات - إظهار إيصالات المصروفات - إيصالات المصروفات - لتلقي إيصالات النفقات ، يجب أن تكون متصلاً بالخادم. + تم إدخال رمز PUK بشكل غير صحيح. + إيصالات النفقات + عرض إيصالات التكلفة + إيصالات النفقات + لتلقي إيصالات النفقات، يجب أن تكون متصلاً بالخادم. التوصيل - لا إيصالات حساب + لا إيصالات التكلفة تعطيل إلغاء - تعطيل وظيفة - سيؤدي هذا إلى حذف جميع الإيصالات من هذا الجهاز ومن الخادم. - استلام إيصالات المصروفات - يتم أيضًا حفظ إيصالات التكلفة الخاصة بك على خادم الوصفات الطبية. - يستلم + تعطيل الوظيفة + سيؤدي هذا إلى حذف كافة إيصالات النفقات من هذا الجهاز والخادم. + استلام إيصالات التكلفة + يتم أيضًا حفظ إيصالات التكلفة الخاصة بك على خادم الوصفات. + تلقى الإجمالي: %s %s الاختيار ينقسم @@ -832,47 +831,73 @@ يُقدِّم %s € السعر الكلي - نصيحة: أرسل إيصالات النفقات عبر تطبيق التأمين - قم بإرسال إيصالات التكلفة بسهولة عبر تطبيق شركة التأمين الخاصة بك. في الخطوة التالية ، حدد هذا التطبيق واضغط على مشاركة. + نصيحة: أرسل إيصالات التكلفة عبر تطبيق التأمين + أرسل إيصالات التكلفة بسهولة عبر تطبيق شركة التأمين الخاصة بك. في الخطوة التالية، حدد هذا التطبيق واضغط على مشاركة. عياده الصيدلية تاريخ عرض المزيد - معرف الدواء + معرف المخدرات أصدرت ل KVNR: %s تاريخ الميلاد: %s موافق - كيف ترسل الإيصالات؟ - التحويل مباشرة إلى التطبيق الخاص بشركة التأمين / مكتب المساعدة. للقيام بذلك ، حدد التطبيق في الصفحة التالية. + كيف يمكنك تقديم المستندات الداعمة؟ + قم بالتحويل مباشرة إلى تطبيق مكتب التأمين/الإعانات الخاص بك. للقيام بذلك، حدد التطبيق في الصفحة التالية. أو - احفظ الملف واستورده لاحقًا إلى بوابة التأمين / المساعدة. - مقال: %s - الرقم: %s + احفظ الملف ثم قم باستيراده لاحقًا إلى بوابة التأمين/المزايا. + المقالة: %s + العدد: %s ضريبة القيمة المضافة: %s %% السعر الإجمالي باليورو: %s رسوم اضافية رسوم خدمة الطوارئ - رسوم BTM - رسوم الوصفة T. - تكاليف الشراء + رسوم بي تي ام + رسوم الوصفة الطبية + تكاليف المشتريات خدمة المراسلة الإجمالي باليورو: %s الجبايه - هل ترغب حقًا في الحذف؟ - سيتم حذف الملف من جهازك ومن الخادم. + حقا حذف؟ + سيتم حذف الملف من جهازك والخادم. حذف - تم النشر + نشر الرمز البريدي المكان - الرجاء إدخال الرمز البريدي الخاص بك للاتصال بنا. - الرجاء إدخال مكان إقامتك عند الاتصال بنا. + يرجى تقديم الرمز البريدي الخاص بك للاتصال بنا. + يرجى الإشارة إلى مكان إقامتك للاتصال بنا. سيتم استبدالها لك تم استبدالها من أجلك يجب عليك تسجيل الدخول لاستخدام هذه الخدمة. APP من التأمين الصحي بطاقة صحية مطلوب رقم التعريف الشخصي المرتبط + لا يمكن استبدالها إلا غدًا كدافع ذاتي + لم يتبق سوى %s من الأيام للاسترداد كدافع ذاتي + \nلا تزال قابلة للاسترداد كدافع ذاتي لمدة %s من الأيام\n + صالح لمدة %s من الأيام فقط + \nصالح لمدة %s من الأيام المتبقية\n + صالحة غدا فقط + سيتم احتساب رسوم اضافية + يأخذ التأمين + تم نقل الوصفة (الوصفات) بنجاح. + لا يمكن معالجة الوصفة. حاول مرة اخرى. قد تحتاج إلى اختيار صيدلية مختلفة. + لا يمكن معالجة الوصفة. أبلغت الصيدلية عن خطأ غير معروف. إذا لزم الأمر، حاول صيدلية أخرى. + تم رفض الوصفة الطبية من الصيدلية. قد تكون الوصفة الطبية غير صالحة أو قد يكون عنوان التسليم أو معلومات الاتصال الخاصة بك غير صالحة. + غير قادر على الاسترداد، يرجى التحقق من اتصالك بالإنترنت. + تم نقل الوصفة بنجاح. ومع ذلك، أبلغت الصيدلية عن خطأ في المعالجة. يرجى الاتصال بالصيدلية. + تم رفض الوصفة الطبية من الصيدلية. لقد تم بالفعل صرف الوصفة الطبية. + تم رفض الوصفة الطبية من الصيدلية. تم حذف الوصفة. + لا يمكن نقل الوصفة. يرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى. + لا يمكن نقل وصفة واحدة أو أكثر. + خطأ في الإرسال + تم الشحن بنجاح! + خطأ في الصيدلية + خطأ في الصيدلية + اتصل بالصيدلية + وصفة طبية تم استردادها بالفعل + تم حذف الوصفة + لا انترنت لتلقي سجلات الوصول، يجب أن تكون متصلاً بالخادم. لا يزال بإمكانك صرف الوصفة الطبية من الصيدلية خلال هذه الفترة، ولكن سيتعين عليك دفع سعر شراء الدواء بالكامل بنفسك. وبدلاً من ذلك، يمكنك أن تطلب من عيادتك إعادة إصدار الوصفة الطبية. تم @@ -881,4 +906,13 @@ في التطبيق قم بمسح هذا الرمز ضوئيًا في الصيدلية الخاصة بك. طلب تصحيح الفواتير + الدواء + الرجاء إدخال حرف واحد على الأقل. + أو. جرب التطبيق في الوضع التجريبي + الوضع التجريبي + الوضع التجريبي + استخدم الوضع التجريبي + تم تفعيل الوضع التجريبي + ينتهي هنا + تفعيل الوضع التجريبي diff --git a/android/src/main/res/values-ar/strings_kbv_codes.xml b/app/features/src/main/res/values-ar/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values-ar/strings_kbv_codes.xml rename to app/features/src/main/res/values-ar/strings_kbv_codes.xml diff --git a/android/src/main/res/values-bg/strings.xml b/app/features/src/main/res/values-bg/strings.xml similarity index 66% rename from android/src/main/res/values-bg/strings.xml rename to app/features/src/main/res/values-bg/strings.xml index 78c1b2e2..bb9d3ddd 100644 --- a/android/src/main/res/values-bg/strings.xml +++ b/app/features/src/main/res/values-bg/strings.xml @@ -1,15 +1,15 @@ Добре - Прекъсване - Връщане + Отказ + обратно наоколо Дигитален. Бърз. Сигурно. ID на задачата - код за достъп + Код за достъп Условия за ползване Защита на данни - рецепти + Рецепти Достъпът до камерата е отказан За да използвате скенера, трябва да разрешите на приложението достъп до вашата камера в системните настройки. Фокусирайте камерата върху код на рецепта @@ -19,89 +19,89 @@ %s рецепта е разпозната %s разпознати рецепти - Прекъсване - светлина на камерата - Да се отмени сканирането? + Отказ + Светлина на камерата + Да се ​​отмени сканирането? Добре Не отменяй - Ето ни + Да тръгваме От какво имаш нужда: Въведете номера за достъп до картата въведете ПИН кода - опитай пак + Опитай пак Неуспешно свързване със сървъра. - Имате още %s опита, преди картата ви да бъде блокирана. + Имате още %s още един опит, преди картата ви да бъде блокирана. Имате още %s опита, преди картата ви да бъде блокирана. - Ще намерите номера за достъп горе вдясно на вашата здравна карта. - Прекъсване - Търсене на карта... - Дръжте здравната карта на гърба на вашето устройство. + Можете да намерите номера за достъп в горния десен ъгъл на вашата здравна карта. + Отказ + Търсене по карта... + Дръжте здравната карта срещу гърба на вашето устройство. Все още се търси... - Бавно преместете картата в задната част на устройството. + Бавно преместете картата на гърба на устройството. Бакшиш Калъфите на устройства може да затруднят свързването чрез NFC. - разпозната карта + Картата е разпозната Опитайте се да не местите здравната карта. Намерена е здравна книжка. Моля те не мърдай. връзката е изгубена - Задръжте отново здравната си карта на гърба на устройството + Задръжте отново здравната си карта срещу гърба на устройството Версия: %s Хеш компилация: %s - меню за отстраняване на грешки + Меню за отстраняване на грешки Отворено до %s Отворен през целия ден отпечатък редактор gematik GmbH\n Фридрихщрасе 136\n 10117 Берлин - Управляващ директор: Dr. медицински Маркус Лейк-Дийкен\n Регистрационен съд: окръжен съд Берлин-Шарлотенбург\n Номер в търговския регистър: HRB 96351\n Идентификационен номер за данък върху продажбите: DE241843684 + Управляващ директор: Dr. мед. Маркус Лейк Дикен\n Регистрационен съд: Окръжен съд Берлин-Шарлотенбург\n Номер в търговския регистър: HRB 96351\n Идентификационен номер по ДДС: DE241843684 Отговаря за съдържанието - д-р медицински Маркус Лейк-Дикен + д-р мед. Маркус Лейк Дикен Контакт Забележете - Стремим се да използваме полово неутрален език. Ако забележите някакви грешки, очакваме с нетърпение да ни изпратите имейл. + Стремим се да използваме език, съобразен с пола. Ако забележите грешки, ще се радваме да ни изпратите имейл. Модерната германска платформа за цифрова медицина - пишете поща - отворен уебсайт + Пишете имейл + Отворете уебсайта Добре дошли Започнете регистрация - отключване + Отключи Регистрирам - Прекъсване + Отказ Сигурност Законни отпечатък защита на данни Условия за ползване - подробности + Подробности Маркирайте като изкупени Маркирайте като неизкупени - доза от - размер на опаковката + Доза от + Размер на опаковката Осигурено лице Фамилия адрес рождена дата - Здравни осигуровки / Платци + Здравноосигурен/платец състояние - осигурителен номер - Предписващо лице + Осигурителен номер + Предписващ Фамилия - Медицински специалист + лекар специалист Номер на лекар (LANR) институция Фамилия адрес - Номер на бизнес помещение - телефонен номер - пощенски адрес - трудова злополука - ден на инцидента + Номер на завода + Телефонен номер + Имейл адрес + Трудова злополука + Ден на инцидента Номер на компания или работодател при злополука - Искате ли да изтриете завинаги тази рецепта? - Гасете - Прекъсване + Искате ли да изтриете тази рецепта за постоянно? + Изтрий + Отказ работно време уебсайт Може да се използва само днес като самоплащащ се @@ -114,58 +114,58 @@ Искате ли да маркирате рецептите като изкупени? Не е изкупено Изкупен - Отваря в %s + Отваря в %s време +49 800 277 377 7 Техническа гореща линия - Отворете скенера за рецепти - Идеи + Отворете скенер за рецепти + Настройки Потискане на екранни снимки - Предотвратява показването на миниатюра при превключване на приложения - Позволявате ли на електронната рецепта да анализира поведението ви при използване анонимно? + Предотвратява показването на изображение за визуализация при превключване на приложения + Позволявате ли на E-Prescription да анализира анонимно вашето поведение при използване? Техническа информация Сигурност на вашите данни за рецепта - Моля, уверете се, че лицата, с които можете да споделяте това устройство и чиито биометрични характеристики може да се съхраняват на това устройство, също имат достъп до вашите рецепти. + Моля, уверете се, че хората, с които можете да споделяте това устройство и чиито биометрични характеристики може да се съхраняват на това устройство, също имат достъп до вашите рецепти. изпращането е неуспешно - Няма настроена програма за електронна поща + Няма настроена програма за имейл Няма резултати Не можахме да намерим резултати за тази дума за търсене. Лицензи с отворен код Контакт - Обадете се на гореща техническа линия + Обадете се на горещата техническа линия Участвайте в анкета +49 800 277 377 7 Искам да помогна да направим това приложение по-добро - Това включва информация за хардуера и софтуера на вашия телефон, настройки за приложението за електронна рецепта и количеството употреба, но никога никакви данни за вас или вашето здраве. - Данните се предоставят на gematik GmbH само от обработващия данни и се изтриват най-късно след 180 дни. Можете да деактивирате анализа отново по всяко време в менюто на приложението. - Тези данни ни позволяват да разберем кои функции се използват често и да ги подобрим. Освен това можем да преценим колко дълго трябва да се поддържа по-стара технология и кога можем например да направим по-нова версия на операционната система задължителна, без да засягаме (твърде много) потребители. - подобряване на приложението + Това включва информация за хардуера и софтуера за вашия телефон, настройките на приложението за електронна рецепта и степента на използване, но никога данни за вас или вашето здраве. + Данните ще бъдат предоставени на gematik GmbH само от обработващата страна и ще бъдат изтрити най-късно след 180 дни. Можете да деактивирате анализа по всяко време в менюто на приложението. + Тези данни ни позволяват да разберем кои функции се използват често и да ги подобрим. Можем също така да преценим колко дълго трябва да се поддържа по-стара технология и кога можем например да направим по-нова версия на операционната система задължителна, без да засягаме (твърде много) потребители. + Подобрете приложението Анонимният анализ остава деактивиран %s Благодарим ви за подкрепата! Регистрирам Моля, идентифицирайте се, за да изтеглите рецепти. - Бележка за аптеките: Получаваме данните за контакт и информацията за аптеките от mein-apothekenportal.de на Германската фармацевтична асоциация. Открили ли сте грешка или искате да коригирате данните? + Бележка за аптеките: Получаваме данните за контакт и информация за аптеките от mein-apothekenportal.de на Германската фармацевтична асоциация. Открили ли сте грешка или искате да коригирате данните? Научете повече - аптеки + Аптеки За съжаление това не проработи \uD83D\uDE15 Моля, опитайте отново. Въведете паролата По-нататък Достъпност - увеличение - Позволява ви да увеличите приложението чрез прищипване или разтваряне на пръстите си (pinch-to-zoom). + мащабиране + Позволява ви да увеличите приложението чрез щипка за мащабиране. парола Защитете данните си с парола по ваш избор. парола - Запазете на компютър + Запазване Покажи парола Повтори паролата Препоръки: %s - пишете поща - Когато изпратите вашето съобщение, ще бъде предадена следната информация за използвания хардуер и операционна система: + Пишете имейл + Когато изпратите вашето съобщение, се предава следната информация за използвания хардуер и операционна система: Осребряване само на място Все още не можете да изпращате електронни рецепти до тази аптека. В момента отворен - куриерска услуга + Messenger услуга Пратка филтър Филтър @@ -173,46 +173,46 @@ Разбрах Повтарящи се пароли Грешка 20 10 76631 - Вашата здравна карта е невалидна. Вашата карта изтекла ли е? Моля, свържете се с вашата здравна каса. + Вашата здравна карта е невалидна. Може би вашата карта е изтекла? Моля, свържете се с вашата здравноосигурителна компания. Неуспешни опити за влизане - Открити са %s неуспешни опита за влизане. + Бяха открити %s неуспешни опита за влизане. Открити са %s неуспешни опита за влизане. Изберете най-доброто архивиране на устройството - Това може да бъде пръстов отпечатък, модел на плъзгане или подобен - токени - жетон за достъп + Това може да бъде пръстов отпечатък, модел на плъзгане или нещо подобно + Токени + Токени за достъп SSO токени Няма наличен маркер за достъп няма наличен SSO токен - копиран в клипборда + копирани в клипборда Щракнете, за да копирате токена в клипборда Важи само днес Позволява няма връзка със сървъра Моля опитайте отново след няколко минути Заредете отново - покажи токени + Показване на жетони Как искате да защитите приложението? Забележете За това устройство не е настроено резервно копие - Препоръчваме ви допълнително да защитите медицинските си данни със сигурност на устройството, като парола или биометрия. + Препоръчваме ви допълнително да защитите медицинската си информация със сигурност на устройството, като например код или биометрия. Не показвайте това известие отново в бъдеще. Свързването е неуспешно. Не може да се установи мрежова връзка. Комуникацията със сървъра е неуспешна: код на състоянието %s . - Неуспешна комуникация със сървъра: Моля, проверете интернет връзката и настройките за час/дата. + Комуникацията със сървъра е неуспешна: Моля, проверете интернет връзката и настройките за час/дата. внимание Вашето устройство може да има намалена защита - Това може да бъде причинено например от манипулирани устройства или активиран режим за разработчици. От съображения за сигурност не препоръчваме да използвате приложението на джейлбрейкнати устройства. - Признавам повишения риск и все пак искам да продължа. + Това може да бъде причинено, например, от манипулирани устройства или когато режимът за разработчици е включен. Препоръчваме да не използвате приложението на джейлбрейкнати устройства от съображения за сигурност. + Признавам повишения риск и все пак бих искал да продължа. Защо устройствата с root достъп представляват потенциален риск за сигурността? Научете повече https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html - Профилно име + Име на профила Моля, въведете име за новия профил. - профилно име - профили + Профилно име + Профили Как да разпознаете здравна карта с активиран NFC Не е възможен контакт чрез това приложение Моля, използвайте обичайните канали, за да се свържете с вашата застрахователна компания. @@ -220,12 +220,12 @@ само ПИН Регистрация в приложението за електронна рецепта Полето за име не може да бъде празно. - Вече съществува профил с въведеното име. + Вече съществува профил с въведеното от вас име. профил %s избрано Цвят на фона - пролетно сиво - съсънка + Пролетно сиво + Росичка То! Е! Розово! Дърво Синя луна септември @@ -233,25 +233,25 @@ Завързани заедно Последна връзка на %s Изтриване на профил? - Това ще изтрие всички данни от профила на това устройство. Вашите рецепти в здравната мрежа ще останат непокътнати. - Гасете - Прекъсване + Това ще изтрие всички данни от профила на това устройство. Вашите рецепти в здравната мрежа ще бъдат запазени. + Изтрий + Отказ изтриване на профил Искате да изтриете последния профил. Приложението изисква поне един профил. Моля, въведете име за новия профил. Грешка 20 10 76831 - Указателят със здравни карти не можа да бъде достигнат. Моля, опитайте отново. - В Националния здравен портал можете да намерите експертно проверена информация за заболявания, кодове по МКБ и по проблемите на профилактиката и грижите. - Отворете Gesund.bund.de + Указателят на здравните карти не можа да бъде достигнат. Моля, опитайте отново. + В Националния здравен портал можете да намерите експертно проверена информация за заболявания, МКБ кодове и теми за профилактика и грижи. + Отворете Health.bund.de Променихме политиката за поверителност Приложението за електронна рецепта се разви. Това наложи актуализирането на нашата политика за поверителност. Отворена политика за поверителност - Това се промени след %s : + Това се промени от %s : Какво се случва, когато отворите приложението? Какво се случва, ако използвам функцията на камерата/чета рецепти с камерата? Няма налични нови рецепти - %s нова рецепта + Новата рецепта %s %s нови рецепти Може да се изкупи @@ -261,18 +261,18 @@ Преглед на регистрационните файлове за достъп Кой и кога получи достъп до вашите рецепти? Ключ за достъп до услугата по рецепта - регистрационни файлове за достъп + Регистри за достъп Няма регистрационни файлове за достъп Все още няма регистрационни файлове за достъп. Рецептата в момента се изпълнява и не може да бъде изтрита Приеми Явно това не проработи - Наясно сме, че връзката със здравната карта има своите подводни камъни. Следователно в бъдеще регистрацията трябва да е възможна и чрез вече удостоверено приложение за здравно осигуряване. \n\n Също така работим върху възможността за изкупуване на рецепти дигитално без регистрация. \n\n Забелязахте ли нещо по време на този процес, което бихте искали да споделите с нас? Моля, пишете ни, ние също се радваме да получим много критични отзиви. + Наясно сме, че връзката със здравната карта има своите подводни камъни. В бъдеще регистрацията трябва да е възможна и чрез вече удостоверено приложение за здравно осигуряване. \n\n Ние също така работим върху това да гарантираме, че рецептите могат да бъдат изкупени цифрово без регистрация. \n\n Забелязахте ли нещо по време на този процес, което бихте искали да споделите с нас? Моля, пишете ни, ние също ще се радваме да получим много критична обратна връзка. Съвети за свързване Подобрете силата на връзката Ако е необходимо, отстранете защитния капак. - Ако устройството вибрира и след това прекъсне връзката, потърсете оптималната позиция в малък радиус. - Преместете устройството по картата много бавно. + Ако устройството вибрира и след това връзката прекъсне, потърсете оптималната позиция в малък радиус. + Преместете устройството много бавно по картата. Поставете устройството директно върху картата. За да направите това, поставете здравната карта върху равна повърхност (напр. маса). Подобрете силата на връзката @@ -282,59 +282,59 @@ Следващ съвет По-нататък Близо - Опитай + Опитвам пишете ни - Лиценз за търсене в аптека - изкупувам + Търсене на аптека с лиценз + Откупете Сканирана рецепта Сканирано на %s Маркирано като осребрено на %s Как искате да продължите? Поръчка Наличен скоро - Резервирайте сега за вземане или го доставете с куриерска служба или доставка - Запазете за по-късна поръчка + Резервирайте сега за вземане или го доставете чрез куриер или доставка + Запазете за последваща поръчка Запазете рецепти на устройството Продължете с %s рецепта Продължете с %s рецепти - Неуспешно свързване на здравна карта + Неуспешна връзка със здравната карта Текущият профил вече е свързан с друга здравна карта (здравноосигурителен номер %s ). - Вашата здравна карта вече е свързана с друг профил. Превключване към профил %s . - Запазете на компютър - данни за контакт и адрес + Вашата здравна карта вече е свързана с друг профил. Отидете на профил %s . + Запазване + Данни за контакт и адрес Контакт - телефонен номер - Моля, дайте телефон за връзка. - Пощенски адрес (по избор) + Телефонен номер + Моля, посочете телефонен номер за връзка с нас. + Имейл адрес (по избор) адрес за доставка - име и фамилия - Моля, въведете име и фамилия за контакт. + Име и фамилия + Моля, посочете име и фамилия, за да се свържете с нас. Улица и номер на къща - Моля, въведете улица и номер на къща, за да можем да се свържем с нас. + Моля, посочете улица и номер на къща, за да се свържете с нас. Допълнителен адрес (по избор) - Инструкция за доставка (по избор) - Необходима е допълнителна информация за контакт + Инструкции за доставка (по избор) + Необходими са допълнителни данни за контакт Отхвърлите промените? изхвърлям За търсене аптечният указател използва геокоординати, определени с помощта на OpenStreetMap. Благодарим на проекта за тази помощ. - © OpenStreetMap ( %s ) + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Поверителност и използване + Защита на данните и използване По-нататък Получихте своя ПИН в писмо от вашата здравноосигурителна компания. Не е получен PIN ПИН код - Проверете връзката с интернет и настройката за час/дата на вашето устройство. + Проверете интернет връзката на вашето устройство и настройките за час/дата. За да влезете, натиснете „Отключи“. - заключен? Моля, проверете биометричните си данни на това устройство. + Заключен? Моля, проверете биометричните си данни на това устройство. Забравена парола? Моля, изтрийте приложението и го инсталирайте отново. Можете да разберете защо в нашия %s . зона за помощ - размер на опаковката и единица + Размер на опаковката и единица активна съставка Количество на активната съставка - обозначение на партидата + Име на партидата Exp категория Ваксина @@ -350,35 +350,35 @@ Паролата не се вижда биометрия парола - в очакване на отговор + Чакам отговор Без рецепти Понастоящем нямате рецепти за обратно изкупуване. - Да се актуализира + Да се ​​актуализира Автоматично излизане От съображения за сигурност връзката със сървъра за рецепти се прекъсва след 12 часа. Свържете се отново, за да получите текущи рецепти. Свържете се - Получихте ли хартиено копие? + Получихте ли разпечатка на хартия? Добавете рецепти към вашия списък, като докоснете бутона за сканиране в горния десен ъгъл. Сканиране на разпечатка на хартия - Трябва да сте влезли, за да получавате рецепти автоматично. + За да получавате рецепти автоматично, трябва да сте влезли в системата. Регистрирам Няма изкупени рецепти Вашите изкупени рецепти се показват тук. От съображения за защита на данните вашите рецепти ще бъдат изтрити от сървъра за рецепти след 100 дни. Няма изкупени рецепти - Вашите изкупени рецепти се показват тук. Добавете рецепти чрез сканиране, за да започнете изкупуването. - управление на устройството + Вашите изкупени рецепти се показват тук. Добавете рецепти чрез сканиране, за да започнете осребряването. + Управление на устройството Свързани устройства Регистриран от %s (това устройство) Регистриран от %s - От съображения за сигурност връзката със сървъра за рецепти се прекъсва след 12 часа. За да се свържете отново, имате нужда от вашата здравна карта и ПИН за всеки процес на свързване. + От съображения за сигурност връзката със сървъра за рецепти се прекъсва след 12 часа. За да се свържете отново, ще ви трябва здравна карта и ПИН за всеки процес на свързване. ПИН код - Въведете своя ПИН (здравна карта). + Въведете ПИН (здравна карта). По-нататък Регистрирам Свързани устройства - премахнете устройството? - Прекъсване - Премахнато + Премахване на устройството? + Отказ + Премахване Премахване на това устройство? Искате ли да премахнете %s ? Ако премахнете %s , връзката със сървъра за рецепти ще бъде окончателно прекъсната най-късно след 12 часа. @@ -391,32 +391,32 @@ wwweg… Няма интернет връзка. Лекарства и превързочни материали - наркотици - Доставка на лекарства с рецепта съгласно § 4 АМВВ + Наркотици + Отпускане на лекарства с рецепта съгласно раздел 4 AMVV Имаш ли нужда от помощ? Събрахме няколко съвета за решаване на най-често срещаните проблеми. Стартирайте съвети за свързване - отключване + Отключи картата е блокирана ПИН кодът е въведен неправилно три пъти. Следователно вашата карта е блокирана от съображения за сигурност. - отключи картата + Отключи картата Въведете PUK С вашия ПИН вие сте получили 8-цифрен PUK от вашата застрахователна компания. Изберете нов ПИН Можете сами да изберете своя нов персонален идентификационен номер (ПИН) (6 до 8 цифри). - PIN запомнен? - Моля, запишете своя ПИН и го съхранявайте на сигурно място. - Прекъсване + Помните ли своя ПИН? + Моля, запишете своя ПИН и го запазете на сигурно място. + Отказ Добре Отключването не е възможно - Достигнали сте максималния брой отключвания на карти с този PUK или сте го въвели многократно неправилно. Моля, свържете се с вашата застрахователна компания. + Достигнали сте максималния брой отключвания на карти с този PUK или многократно сте го въвели неправилно. Моля, свържете се с вашата застрахователна компания. Можете да използвате един PUK за до 10 отключвания. - картата е отключена + Картата е отключена От какво имаш нужда: - вашата здравна карта + Вашата здравна карта PUK на вашата здравна карта По-нататък - здравна карта + Здравна карта Поръчайте ПИН или карта Регистрирам Как искате да влезете? @@ -426,85 +426,85 @@ Запиши се сега Или: Влезте с %s . Вашето приложение за здравно осигуряване - „Вашият номер за достъп може да бъде намерен в горния десен ъгъл на вашата здравна карта.“ + „Можете да намерите вашия номер за достъп в горния десен ъгъл на вашата здравна карта.“ Картата ми няма номер за достъп - Имате още %s опита, преди картата ви да бъде блокирана. + Имате още %s още един опит, преди картата ви да бъде блокирана. Имате още %s опита, преди картата ви да бъде блокирана. Поставете здравната карта на гърба на телефона Следващият процес може да отнеме до 30 секунди. Поставете карта %s на гърба на телефона. - в горния десен ъгъл - в горната среда - в горния ляв ъгъл + в горната дясна област + в средата в горната част + в горната лява зона в средната зона вдясно средата - в центъра вляво + в средната зона вляво в долната дясна област - в долния център - в долния ляв ъгъл + в средата в долната зона + в долната лява зона Помогне Изпратено преди %s минути Изпратено на %s Изпратено току що - Изпратено в %s часа + Изпратено в %s време Вече не е валиден - Влезте с приложението - изберете застраховка + Регистрирайте се с приложението + Изберете застраховка Не намерихте това, което търсихте? Този списък непрекъснато се разширява. Регистрацията със здравна карта вече се поддържа от всяка здравноосигурителна компания. Обратна връзка от приложението за електронна рецепта - Очакваме вашите отзиви. Моля, използвайте мястото по-долу и бъдете възможно най-точни: + Очакваме вашите отзиви. Моля, използвайте следното място и бъдете възможно най-точни: PUK Близо Колко жалко… - За съжаление вашето устройство не отговаря на минималните изисквания за влизане в приложението за електронна рецепта. Необходими са поне Android 7 и NFC чип за сигурно удостоверяване със здравната ви карта. + За съжаление вашето устройство не отговаря на минималните изисквания за регистрация в приложението за електронна рецепта. За сигурно удостоверяване с вашата здравна карта са необходими поне Android 7 и NFC чип. Научете повече Запазване на данните за вход? - Запазете на компютър + Запазване Не спестявайте Забележете - От съображения за сигурност връзката със сървъра за рецепти се прекъсва след 12 часа. За да се свържете отново, имате нужда от здравна карта и ПИН за всеки процес на свързване. + От съображения за сигурност връзката със сървъра за рецепти се прекъсва след 12 часа. За да се свържете отново, ще ви трябва здравна карта и ПИН за всеки процес на свързване. Настройте биометрична защита - Запазването на данните за достъп не е възможно. Настройте биометрична защита (напр. пръстов отпечатък) на вашето устройство предварително. - Прекъсване - Идеи + Не е възможно да се запазят данни за достъп. Предварително настройте биометрична защита (напр. пръстов отпечатък) на вашето устройство. + Отказ + Настройки Забележете Приеми Сигурност на вашите данни за рецепта - \"Това приложение използва най-сигурния биометричен сензор, осигурен от вашето устройство, за да съхранява вашите идентификационни данни в защитена зона на паметта на устройството.\" - Биометричната сигурност на вашите данни за достъп ви позволява да отваряте това приложение в бъдеще, без да въвеждате своя ПИН и здравна карта, както и да преглеждате, извиквате, осребрявате или изтривате рецепти. - Моля, уверете се, че лицата, с които можете да споделяте това устройство и чиито биометрични характеристики може да се съхраняват на това устройство, също имат достъп до вашите рецепти. + \"Това приложение използва най-сигурния биометричен сензор, предоставен от вашето устройство, за да защити вашите идентификационни данни в защитена зона на хранилището на устройството.\" + Биометричната сигурност на вашите данни за достъп ви позволява да отваряте това приложение в бъдеще, да преглеждате, извличате, осребрявате или изтривате рецепти без здравна карта и въвеждане на своя ПИН. + Моля, уверете се, че хората, с които можете да споделяте това устройство и чиито биометрични характеристики може да се съхраняват на това устройство, също имат достъп до вашите рецепти. това за съжаление не проработи - Удостоверяването с приложението за здравно осигуряване беше неуспешно. + Удостоверяването с приложението за здравно осигуряване не беше успешно. Изтекъл на %s Рецептата вече е изтрита от сървъра Моля, коригирайте въведеното или отхвърлете промените Правилно - данни за застрахования + Данни за осигуреното лице Фамилия Застраховка - осигурителен номер - номер за достъп до картата + Осигурителен номер + Номер за достъп до картата Регистрирам - Дерегистрирайте се - Запазете на компютър + Излез от профила си + Запазване промяна Редактиране на профилна снимка По-нататък сървърът не отговаря Моля, опитайте отново по-късно. Опитай пак - Потърсете застраховка + Търсете застраховка Свързване към сървъра за рецепти сега? Влязохте успешно връзката е изгубена Свързване към сървъра за рецепти сега? Без жетони - Ще получите токен, когато влезете в услугата за рецепти.\n - поръчки + Ще получите токен, когато влезете в услугата за рецепти.\n + Поръчки Изберете желания PIN - отключи картата + Отключи картата Изберете PIN Повторете PIN Записите се различават един от друг. @@ -513,13 +513,13 @@ Точно сега В %s часа Количката за пазаруване е готова - Рецептата е добавена към вашата пазарска количка. Моля, посетете уебсайта на аптеката, за да завършите поръчката. + Рецептата е добавена във вашата количка. Моля, посетете уебсайта на аптеката, за да завършите поръчката. Отворете количката за пазаруване - Покажете този код за получаване в аптеката. - Получаване на код за получаване + Покажете този код за събиране в аптеката. + Кодът за получаване е получен Съобщението не може да се покаже Моля, свържете се с вашата аптека ( %s ). - Показване на връзката към количката + Показване на връзката към пазарската количка Показване на кода за получаване Покажете съобщението %s в %s часа @@ -527,7 +527,7 @@ Преглед на поръчката Нов курс - Поръчка + Поръчката Безплатно за обаждащия се. Работно време: понеделник - петък 8:00 - 20:00 с изключение на национални празници Аптека Изберете желания PIN @@ -535,42 +535,42 @@ В момента отворен и близо до мен Филтриране по... започнете търсене - директно възлагане - аптеки - телефонен номер (по избор) + Директно възлагане + Аптеки + Телефонен номер (по избор) Търсене по име или адрес Няма валидна информация за аптеката Няма открита актуална информация за тази аптека. Записът за тази аптека ще бъде изтрит. Добре - Указателят на аптеката не е наличен - В момента не може да бъде извлечена актуална информация за тази аптека. Моля, проверете вашата интернет връзка. - Прекъсване + Указателят на аптеките не е наличен + В момента няма достъп до актуална информация за тази аптека. Моля, проверете вашата интернет връзка. + Отказ Опитай пак Запазване на околната среда Влизането не е възможно - Изглежда, че вашите биометрични данни за вход са се променили. Моля, регистрирайте се отново със здравната си карта. - Прекъсване + Изглежда биометричните ви характеристики за влизане са се променили. Моля, влезте отново със здравната си карта. + Отказ Регистрирам - профил 1 + Профил 1 Близо до мен Може да се изкупи по-късно Може да се използва от %s - подобрения на продукта + Подобрения на продукта Анонимен анализ - Помогнете ни да направим това приложение по-добро. Всички данни за използването се събират анонимно и се използват само за подобряване на потребителското изживяване. - сигурност на устройството + Помогнете ни да направим това приложение по-добро. Всички данни за използването се събират анонимно и се използват изключително за подобряване на потребителското изживяване. + Сигурност на устройството лични настройки Достъпност - подобрения на продукта + Подобрения на продукта Добавена рецепта Рецептата вече е налична Възникна грешка при импортирането - Гасете + Изтрий Сканирана рецепта - Възможен заместител + Възможна подготовка за смяна Забравен ПИН - %s рецепта + %s Рецепта %s рецепти Прочетох и приемам политиката за поверителност и условията за ползване. @@ -580,63 +580,62 @@ Подобрете използваемостта. Откриване на грешки и сривове. Разбира се, всички данни се събират анонимно. - Можете да промените това решение в системните настройки по всяко време. + Можете да промените това решение по всяко време в системните настройки. продължи Приеми - Това приложение използва най-сигурния метод, предоставен от вашето устройство. - Запазете на компютър + Това приложение използва най-безопасния метод, предоставен от вашето устройство. + Запазване Избирам лекарство - търговско наименование + Търговско наименование да Не дозировка дата на издаване Тази рецепта ще бъде осребрена за вас като част от лечението. Неопределено - допълнително заплащане + Доплащане лекарство - Бележки за доставка + Инструкции за подаване Допустимо според BVG - алтернативна подготовка - име на рецепта + Алтернативна подготовка + Име на рецептата Опаковка - инструкция за изработка + Инструкции за производство Описание дадена от издадена на: активна съставка - предписано + Предписано Получете Какво е директно възлагане? - В случай на директни препоръки, рецепта от практика или болница се изкупува директно в аптека. Застрахованите лица не трябва да предприемат никакви действия и не могат да се намесват в процеса на обратно изкупуване. \n\n Директните препоръки са изброени в приложението за електронна рецепта, за да направи лечението ви по-прозрачно за вас. - такса за аварийно обслужване - Понякога се изисква бързане. Някои рецепти могат да бъдат изкупени без допълнително заплащане на такса за спешно обслужване, като например през нощта или на официални празници. - Лекарства с доплащане - Освободени от доплащане - Задължително здравноосигурените трябва да плащат доплащане до десет евро за лекарства с рецепта. \n\n Размерът на доплащането зависи от цената на вашето лекарство. Вие сами трябва да плащате за лекарства, които струват по-малко от 5 евро.\n За лекарства, които са по-скъпи, трябва да платите десет процента от цената, но най-малко 5 евро и максимум 10 евро. \n\n Децата и младежите под 18-годишна възраст обикновено са освободени от доплащане. \n\n Ако годишните ви разходи за лекарства надхвърлят вашия финансов лимит, можете да бъдете освободени от доплащане. Говорете с вашия здравен застраховател за това. - Вие сте освободени от доплащането на това лекарство. Вашата здравна застраховка ще покрие цената на лекарството. - Колко дълго е валидна тази рецепта? + При директно насочване рецепта от практика или болница се изпълнява директно в аптека. Застрахованите лица не трябва да предприемат никакви действия и не могат да се намесват в процеса на обратно изкупуване. \n\n Директните препоръки са изброени в приложението за електронна рецепта, за да направи лечението ви по-прозрачно за вас. + Такса за спешни услуги + Понякога бързането е необходимо. Някои рецепти могат да бъдат изпълнени без допълнително заплащане на такса за спешна помощ, например през нощта или по празници. + Лекарствата се доплащат + Освободен от допълнително заплащане + Задължително здравноосигурените трябва да доплащат до десет евро за лекарства с рецепта. \n\n Размерът на допълнителното плащане зависи от цената на вашето лекарство. Вие сами трябва да плащате за лекарства, които струват по-малко от 5 евро.\n За лекарства, които са по-скъпи, трябва да платите десет процента от цената, но най-малко 5 евро и максимум 10 евро. \n\n Децата и младежите под 18-годишна възраст обикновено са освободени от допълнително заплащане. \n\n Ако годишните ви разходи за лекарства надхвърлят лимита ви за финансова тежест, можете да бъдете освободени от доплащане. Говорете с вашата здравноосигурителна компания за това. + Вие сте освободени от плащането на доплащане за това лекарство. Вашата здравноосигурителна компания ще покрие разходите за лекарството. + Колко време е валидна тази рецепта? През този период можете да осребрите своята рецепта във всяка аптека с максимално допълнително плащане от 10 евро. - Възможен заместител - Поради законовите изисквания на вашата здравноосигурителна компания, можете да получите алтернатива със същата активна съставка. \n\n Лекарствата могат да изглеждат и да се наричат по различен начин, да имат различни цени и производители, но да съдържат една и съща активна съставка. Самата активна съставка и дозировката са особено важни за действието на лекарствата в организма. Пациентите в аптеката често получават различно лекарство от предписаното от лекаря по рецепта - при условие че лекарствата са сравними. Може да има терапевтични и икономически причини за промяната. + Възможна подготовка за смяна + Поради законови изисквания на вашата здравноосигурителна компания може да ви бъде дадена алтернатива със същата активна съставка. \n\n Лекарствата могат да изглеждат и да се наричат ​​различно, да имат различни цени и производители, но все пак да съдържат една и съща активна съставка. Самата активна съставка и дозировката са от решаващо значение за ефекта на лекарствата в организма. Често пациентите получават в аптеката различно лекарство от предписаното от лекаря – при условие че лекарството е сравнимо. Може да има терапевтични и икономически причини за промяната. Сканирана рецепта - От съображения за сигурност рецептите, импортирани от хартиена разпечатка, не трябва да показват лични или медицински данни. \n\n Влезте в това приложение със здравна карта или застрахователно приложение, за да видите цялата информация, съдържаща се в рецептата. + Рецептите, импортирани от хартиено копие, не могат да показват лична или медицинска информация от съображения за сигурност. \n\n Влезте в това приложение със здравна карта или застрахователно приложение, за да видите цялата информация, съдържаща се в рецептата. Рецептата е неправилна Тази рецепта е издадена неправилно. - Сканирана рецепта - такса за аварийно обслужване + Такса за спешни услуги Дозировка според писмените инструкции телефон - сайт + уебсайт поща Сортирането по разстояние не е възможно. Добре Въведете текущия ПИН - Въведен е неправилен ПИН + Въведен грешен ПИН код Текущият ПИН на вашата здравна карта картата е блокирана - Разблокирайте картата си в Настройки > Деблокиране на карта. + Отключете картата си в Settings > Unlock Card. От съображения за сигурност, моля, въведете текущия си PIN. Забравен ПИН Неправилна рецепта @@ -645,59 +644,59 @@ Докладвай Не сте влезли в системата Регистриран с - здравна карта + Здравна карта биометрия Не сте влезли в системата - Интересуваме се от вашето мнение. Моля, отделете пет минути, за да отговорите на нашата анкета. Благодаря ви предварително. - предупредително съобщение + Интересуваме се от вашето мнение. Моля, отделете пет минути, за да попълните нашата анкета. Благодаря много предварително. + Предупредителна бележка Аптеката е добавена към любими Аптеката е премахната от любимите Моите аптеки Силата на паролата е много добра - Операцията за запис е неуспешна + Операцията за запис не е успешна ПИН не можа да бъде запазен Докладвай Задайте ПИН Правилото за достъп е нарушено Нямате разрешение за достъп до директорията на картата. Задайте свой собствен щифт - Картата е защитена с ПИН от вашата здравноосигурителна компания (транспортен ПИН), моля, задайте свой собствен ПИН. + Картата е защитена с ПИН от вашата здравноосигурителна компания (транспортен ПИН). Моля, въведете своя ПИН. Паролата не е намерена На вашата карта няма запазена парола. Вие сте излезли Влезте отново, за да актуализирате вашите рецепти. - номер на активната съставка + Номер на активната съставка сила и единство Осребрено преди %s минути Осребрено на %s Изкупено току-що Осребрено в %s часа - поръчки - Тази рецепта е изкупена за вас като част от лечение. - такса за аварийно обслужване - Тази рецепта не може да бъде изпълнена през нощта в аптека без допълнително заплащане на такса за спешна помощ. + Поръчки + Тази рецепта е изписана като част от лечение за вас. + Такса за спешни услуги + Тази рецепта не може да бъде изпълнена в аптека през нощта без допълнително заплащане на такса за спешна помощ. Търси тук - Идеи - Споделете местоположението в настройките. + Настройки + Споделете местоположение в Настройки. Близо до мен - Задръжте, за да редактирате името. + Натиснете и задръжте, за да редактирате името. Въведете новото име за профила. - Трябва да сте влезли, за да получавате цифрови рецепти от вашата практика. + За да получавате дигитално рецепти от вашата практика, трябва да сте влезли. Получаване на рецепти дигитално? - Плъзнете екрана надолу, за да опресните. + Издърпайте надолу екрана, за да опресните. Без рецепти Добавете рецепти, като използвате бутона + в горния десен ъгъл. Регистрирам - архив с рецепти + Архив с рецепти Може би по-късно Регистрирам Редактиране на профилна снимка - архив с рецепти + Архив с рецепти Въведи име - Запазете на компютър + Запазване Моята поръчка Получател: в - рецепти + Рецепти Аптека Изпратете промяна @@ -705,22 +704,22 @@ Доставка с куриер Доставка по пощата %s рецепти - Осребряването не е възможно + Не е възможно да се осребри Една или повече рецепти не можаха да бъдат осребрени. Няма избрана рецепта За да осребрите рецепти, трябва да бъде избрана поне една рецепта. - Добавете информация за контакт + Добавете данни за контакт промяна Без рецепта Понастоящем нямате рецепти за обратно изкупуване колекция - куриер + доставчик Пратка - изберете рецепти + Изберете рецепти Докоснете тук, за да сканирате рецепти Натиснете продължително, за да редактирате имена - Добавете още профили, например за вашите деца или родители - Кликнете върху дисплея, за да пропуснете показаната подсказка. + Добавете допълнителни профили, например за вашите деца или родители + Кликнете върху дисплея, за да пропуснете подсказката, която се появява. Как да откупите? Как бихте искали да получите вашето лекарство? Осребрете директно @@ -728,8 +727,8 @@ Поръчка Резервирайте или го доставете Готов - колективен код - единични кодове + Код на колекцията + Индивидуални кодове Имате %s рецепта. Имате %s рецепти. @@ -743,65 +742,65 @@ Забележете Това приложение използва софтуер от Google за разпознаване на кодове. Научете повече - Относно скенера за кодове на рецепти + Информация за скенера за код на рецепта Какви данни съдържа кодът на рецептата? - Кодът на рецептата съдържа само идентификатор на рецептата. Това позволява рецептата да бъде намерена в услугата за рецепти в цифровата здравна мрежа. Кодът на рецептата не съдържа никакви данни за Вас или Вашето лекарство. + Кодът на рецептата съдържа само идентификатор за рецептата. Това означава, че рецептата може да бъде намерена в услугата за рецепти в цифровата здравна мрежа. Кодът на рецептата не съдържа никаква информация за вас или вашето лекарство. Значи никой не може да направи нищо само с кода на рецептата? - Правилно. Данните за рецептата трябва да бъдат изтеглени от службата за рецепти. Това изисква защитено влизане. + Правилно. Данните за рецептите трябва да бъдат изтеглени от службата за рецепти. За това е необходимо защитено влизане. Кой може да се регистрира за услугата по рецепта? - Регистрирането в услугата за рецепти в цифровата здравна мрежа е възможно за осигурени лица, аптеки, медицински практики и болници. + Регистрация за услугата по рецепти в цифровата здравна мрежа е възможна за осигурени лица, аптеки, практики и болници. Защо приложението за електронна рецепта използва функциите на Google? - Google предлага функции, които могат лесно да бъдат вградени в приложения и които непрекъснато се разработват и актуализират от Google. Това гарантира, че функциите работят на много различни крайни устройства и могат да се управляват сигурно. Приложението използва функция за подобряване на функцията на камерата и сканирането за устройства с Android (Google ML Kit). - Как работи подобрението на сканирането на Google ML Kit? + Google предлага функции, които могат лесно да бъдат интегрирани в приложения и които Google непрекъснато разработва и актуализира. Това гарантира, че функциите работят на много различни устройства и могат да се използват безопасно. Приложението използва функция за подобряване на камерата и функционалността за сканиране за устройства с Android (Google ML Kit). + Как работи подобряването на сканирането с Google ML Kit? Google ML Kit помага за оптимизиране на изображението, заснето от камера, така че кодовете на рецептите да могат да се четат дори при условия на лошо осветление или с по-стари модели камери. - Данните за рецептата или моето лекарство ще бъдат ли предадени на Google? - Не. Прочетеният код на рецептата се записва директно в приложението. Няма да бъде предадено на Google. Данните за рецептата не се съхраняват в кода, а само в цифровата здравна мрежа. Оттам те се изпращат до приложението. Google няма достъп до цифровата здравна мрежа. + Данните за рецептата или моето лекарство ще бъдат ли споделени с Google? + Не. Прочетеният код на рецептата се записва директно в приложението. Няма да се споделя с Google. Данните за рецептата не се съхраняват в кода, а само в цифровата здравна мрежа. Оттам те се предават на приложението. Google няма достъп до цифровата здравна мрежа. Какви данни обработва Google, когато използва ML Kit? - Google има достъп само до техническа информация за използваното крайно устройство и общото използване на допълнителната функция (напр. процент грешки, настройки на камерата), за да запише това статистически и по този начин да подобри допълнителната функция. Когато осъществявате достъп, Google временно записва IP адреса на вашето крайно устройство. Информацията за вас и съдържанието на рецептата няма да бъдат записани от Google. + Google получава достъп само до техническа информация за използваното устройство и общото използване на допълнителната функция (напр. процент грешки, настройки на камерата), за да запише това статистически и по този начин да подобри допълнителната функция. При достъп Google временно записва IP адреса на вашето устройство. Информацията за вас и съдържанието на рецептата не се записват от Google. Доброволно ли е използването на Google ML Kit? - да Въпреки това, ML Kit е вграден в скенера за код на рецепта във версията за Android на приложението за електронна рецепта. Ако използвате скенера за код на рецепта на устройство с Android, винаги се използва и функцията ML Kit. Можете обаче да го направите без да използвате скенера за код на рецепта. Вашите рецепти също могат да бъдат заредени в приложението, ако се регистрирате в дигиталната здравна мрежа с електронната здравна карта или чрез вашето приложение за здравно осигуряване. + да Въпреки това, ML Kit е вграден в скенера за код на рецепта във версията за Android на приложението за електронна рецепта. Ако използвате скенера за код на рецепта на устройство с Android, винаги се използва функцията ML Kit. Можете обаче да избегнете използването на скенера за код на рецепта. Вашите рецепти също могат да бъдат заредени в приложението, ако влезете в дигиталната здравна мрежа с електронната здравна карта или чрез вашето приложение за здравно осигуряване. Мога ли да видя кой е гледал моите рецепти? да Целият достъп до вашите данни е изцяло регистриран в цифровата здравна мрежа. В приложението за електронна рецепта можете да видите кой има достъп до вашите данни. - С кого мога да се свържа, ако имам въпроси относно приложението или електронната рецепта? - Можете да намерите подробна информация в декларацията за защита на данните. + Къде мога да се свържа, ако имам въпроси относно приложението или електронната рецепта? + Подробна информация можете да намерите в декларацията за защита на данните. Предписан брой опаковки Без рецепти За това се нуждаете от рецепти, които могат да бъдат изкупени. - изберете застраховка - Потърсете застраховка - Прекъсване + Изберете застраховка + Търсете застраховка + Отказ За какво бихте искали да кандидатствате? - За това приложение се нуждаете от карта и свързания ПИН код. + За това приложение се нуждаете от карта и съответния ПИН. Как бихте искали да се свържете с вашата застрахователна компания? Вашата застрахователна компания предлага следните опции за контакт - Вашата застрахователна компания предлага следните опции за контакт + Вашата застрахователна компания предлага следната опция за контакт Близо ПИН кодът е въведен неправилно. Номерът за достъп е въведен неправилно PUK въведен неправилно. разходни бележки - Показване на квитанции за разходи + Вижте квитанции за разходи разходни бележки За да получавате разписки за разходи, трябва да сте свързани със сървъра. Свържете се - Няма квитанции за разходи + Няма разписки за разходи Деактивирайте - Прекъсване - деактивирайте функцията - Това ще изтрие всички разписки от това устройство и от сървъра. - Получавайте разписки за разходи + Отказ + Деактивирайте функцията + Това ще изтрие всички разписки от това устройство и сървъра. + Получавайте разписки за разходите Вашите разписки за разходи също се записват на сървъра за рецепти. - Получете + получено Общо: %s %s Избирам Сплит - Гасете - Гасете + Изтрий + Изтрий Изпращане %s € обща цена Съвет: Изпратете разписки за разходи чрез застрахователното приложение - Изпращайте разписки за разходи лесно чрез приложението на вашата застрахователна компания. В следващата стъпка изберете това приложение и натиснете Споделяне. + Изпращайте лесно разписки за разходи чрез приложението на вашата застрахователна компания. В следващата стъпка изберете това приложение и натиснете споделяне. Практикувайте Аптека Дата @@ -809,38 +808,64 @@ ID на лекарството Издадена за KVNR: %s - Дата на раждане: %s + Роден на: %s Добре - Как изпращате разписки? - Прехвърлете директно в приложението на вашата застрахователна компания/офис за помощ. За да направите това, изберете приложението на следващата страница. + Как подавате оправдателни документи? + Прехвърлете директно в приложението на вашия офис за застраховка/обезщетения. За да направите това, изберете приложението на следващата страница. или - Запазете файла и по-късно го импортирайте в портала за застраховка/помощ. + Запазете файла и по-късно го импортирайте в портала за застраховки/обезщетения. Статия: %s - Номер: %s + Брой: %s ДДС: %s %% Брутна цена в EUR: %s Допълнителни такси - такса за аварийно обслужване + Такса за спешни услуги BTM такса - T такса за рецепта - разходи за доставка - куриерска услуга + Такса за T-рецепта + Разходи за снабдяване + Messenger услуга Общо в евро: %s такса Наистина ли изтривам? - Файлът ще бъде изтрит от вашето устройство и от сървъра. - Гасете + Файлът ще бъде изтрит от вашето устройство и сървъра. + Изтрий Публикувано Пощенски код Местоположение - Моля, въведете вашия пощенски код, за да се свържете с нас. - Моля, въведете мястото си на пребиваване, когато се свързвате с нас. + Моля, посочете вашия пощенски код, за да се свържете с нас. + Моля, посочете местоживеене, за да се свържете с нас. Ще бъде изкупено за вас Беше осребрено за вас Трябва да сте влезли, за да използвате тази услуга. - застрахователно приложение - здравна карта + Приложение за застраховка + Здравна карта Изисква се свързан ПИН код + Може да се изкупи само утре като самоплащащ + Остават само %s дни за осребряване като самоплащащ + \nВсе още може да се използва като самоплащащ за %s дни\n + Валиден само за %s дни + \nВалиден още %s дни\n + Важи само утре + Прилагат се такси + Поема застраховка + Рецептата(ите) са прехвърлени успешно. + Рецептата не може да бъде обработена. Моля, опитайте отново. Може да се наложи да изберете друга аптека. + Рецептата не може да бъде обработена. Аптеката съобщава за неизвестна грешка. Ако трябва, опитайте в друга аптека. + Рецептата е отхвърлена от аптеката. Рецептата може да е невалидна или вашият адрес за доставка или информация за контакт може да са невалидни. + Не можете да осребрите, моля, проверете връзката си с интернет. + Рецептата беше прехвърлена успешно. Аптеката обаче съобщава за грешка при обработката. Моля, свържете се с аптеката. + Рецептата е отхвърлена от аптеката. Рецептата вече е изкупена. + Рецептата е отхвърлена от аптеката. Рецептата е изтрита. + Рецептата не можа да бъде прехвърлена. Моля, проверете вашата интернет връзка и опитайте отново. + Една или повече рецепти не можаха да бъдат прехвърлени. + Грешка при изпращане + Изпратено успешно! + Грешка в аптеката + Грешка в аптеката + Свържете се с аптеката + Рецептата вече е изкупена + Рецептата е изтрита + Няма интернет За да получавате регистрационни файлове за достъп, трябва да сте свързани към сървъра. Все още можете да изпълните рецептата в аптека в рамките на този период, но ще трябва да заплатите цялата покупна цена за лекарството сами. Като алтернатива можете да поискате от вашата практика рецептата да бъде преиздадена. Готов @@ -849,4 +874,13 @@ В ап Сканирайте този код във вашата аптека. Искане за корекция на таксуването + лекарство + Моля, въведете поне 1 знак. + Или. Опитайте приложението в демо режим + Демо режим + Демо режим + Използвайте демо режим + Демо режимът е активиран + Край тук + Активиране на демо режим diff --git a/android/src/main/res/values-cs/strings.xml b/app/features/src/main/res/values-cs/strings.xml similarity index 65% rename from android/src/main/res/values-cs/strings.xml rename to app/features/src/main/res/values-cs/strings.xml index 6aaddff3..2e4f2536 100644 --- a/android/src/main/res/values-cs/strings.xml +++ b/app/features/src/main/res/values-cs/strings.xml @@ -1,17 +1,17 @@ OK - Přerušit - Vrátit se + zrušení + Zadní kolem Digitální. Rychle. Zajistit. ID úkolu - přístupový kód + Přístupový kód Podmínky použití Ochrana dat - recepty + Recepty Přístup ke kameře odepřen - Chcete-li skener používat, musíte aplikaci povolit přístup k fotoaparátu v nastavení systému. + Chcete-li skener používat, musíte aplikaci povolit přístup k fotoaparátu v Nastavení systému. Zaměřte fotoaparát na kód receptu Toto není platný kód předpisu Tento kód předpisu již byl naskenován @@ -19,153 +19,153 @@ %s recept rozpoznán - %s receptů rozpoznáno + %s receptů zjištěno - Přerušit - světlo fotoaparátu + zrušení + Světlo fotoaparátu Zrušit skenování? OK Nerušit - Tady jsme + Pojďme Co potřebuješ: Zadejte přístupové číslo karty zadejte PIN kód Zkus to znovu Připojení k serveru se nezdařilo. - Máte ještě %s pokusů, než bude vaše karta zablokována. + Máte %s ještě jeden pokus, než bude vaše karta zablokována. Máte ještě %s pokusů, než bude vaše karta zablokována. - Přístupové číslo najdete na své zdravotní kartě vpravo nahoře. - Přerušit - Hledat mapu... - Přiložte zdravotní kartu k zadní straně zařízení. - Stále se hledá… + Přístupové číslo najdete v pravé horní části své zdravotní karty. + zrušení + Hledat podle mapy… + Přidržte zdravotní kartu na zadní straně zařízení. + Stále hledám… Pomalu posuňte kartu na zadní straně zařízení. Spropitné Pouzdra na zařízení mohou ztížit připojení přes NFC. - karta rozpoznána + Karta rozpoznána Snažte se nehýbat zdravotní kartou. Zdravotní průkaz nalezen. Prosím, nehýbej se. spojení ztraceno - Znovu přiložte svou zdravotní kartu k zadní straně zařízení + Znovu přidržte svou zdravotní kartu na zadní straně zařízení Verze: %s - Sestavení hash: %s - menu ladění + Sestavit hash: %s + Nabídka ladění Otevřeno do %s Otevřeno celý den otisk editor gematik GmbH\n Friedrichstrasse 136\n 10117 Berlín - Jednatel: Dr. lékařský Markus Leyck-Dieken\n Rejstříkový soud: okresní soud Berlin-Charlottenburg\n Číslo obchodního rejstříku: HRB 96351\n Daňové identifikační číslo: DE241843684 + Jednatel: Dr. med. Markus Leyck Dieken\n Rejstříkový soud: Okresní soud Berlin-Charlottenburg\n Číslo obchodního rejstříku: HRB 96351\n DIČ: DE241843684 Zodpovědnost za obsah - Dr lékařský Markus Leyck-Dieken + Dr. med. Markus Leyck Dieken Kontakt Oznámení - Snažíme se používat genderově neutrální jazyk. Pokud zaznamenáte nějaké chyby, těšíme se na vaši zprávu prostřednictvím e-mailu. + Snažíme se používat genderově spravedlivý jazyk. Pokud si všimnete nějaké chyby, budeme rádi, když se ozvete e-mailem. Německá moderní platforma pro digitální medicínu - napsat mail - otevřít webovou stránku + Napište email + Otevřete webovou stránku Vítejte Spusťte registraci - odemknout + Odemknout Registrovat - Přerušit + zrušení Bezpečnostní Právní otisk ochrana dat Podmínky použití - podrobnosti + Podrobnosti Označit jako vyplacené - Označit jako neuplatněno - léková forma - velikost balení + Označit jako neuplatněné + Léková forma + Velikost balení Pojištěná osoba Příjmení adresa datum narození - Zdravotní pojištění / Plátci + Zdravotní pojištění/plátce postavení cislo pojistence - Předepisující osoba + Předepisující lékař Příjmení - Lékařský specialista + Specializovaný lékař Číslo lékaře (LANR) instituce Příjmení adresa - Číslo provozovny - telefonní číslo - emailová adresa - pracovní úraz - den nehody + Číslo rostliny + Telefonní číslo + Emailová adresa + Pracovní úraz + Den nehody Číslo havarijní společnosti nebo zaměstnavatele Chcete tento recept trvale smazat? - Uhasit - Přerušit + Vymazat + zrušení Otevírací doba webová stránka Dnes splatný pouze jako samoplátce Registrovat - Aktivujte NFC + Povolit NFC Pro přihlášení pomocí zdravotní karty aktivujte na svém zařízení funkci NFC. aktivovat Opravit - Uplatněné recepty? + Uplatněny recepty? Chcete recepty označit jako uplatněné? - Není vykoupeno + Nevykoupeno Vykoupeno - Otevírá v %s + Otevírá v %s čas +49 800 277 377 7 Technická horká linka Otevřete skener pro recepty - Nápady + Nastavení Potlačit snímky obrazovky - Zabrání zobrazení miniatury při přepínání aplikací - Umožňujete e-receptu anonymně analyzovat vaše chování při používání? + Zabrání zobrazení náhledu při přepínání aplikací + Umožňujete E-Prescription anonymně analyzovat vaše chování při používání? Technické informace Zabezpečení vašich údajů o předpisech - Zajistěte prosím, aby osoby, se kterými můžete toto zařízení sdílet a jejichž biometrické charakteristiky mohou být v tomto zařízení uloženy, měly také přístup k vašim předpisům. + Zajistěte prosím, aby lidé, se kterými můžete toto zařízení sdílet a jejichž biometrické charakteristiky mohou být v tomto zařízení uloženy, měli také přístup k vašim předpisům. odeslání se nezdařilo Není nastaven žádný e-mailový program Žádné výsledky Pro tento hledaný výraz jsme nenašli žádné výsledky. - Open Source licence + Open source licence Kontakt - Zavolejte na technickou horkou linku + Zavolejte na technickou linku Zúčastněte se průzkumu +49 800 277 377 7 Chci pomoci vylepšit tuto aplikaci - To zahrnuje informace o hardwaru a softwaru ve vašem telefonu, nastavení aplikace pro elektronický předpis a množství používání, ale nikdy žádná data o vás nebo vašem zdraví. - Údaje jsou společnosti gematik GmbH zpřístupněny pouze zpracovatelem údajů a jsou vymazány nejpozději po 180 dnech. Analýzu můžete kdykoli znovu deaktivovat v nabídce aplikace. - Tato data nám umožňují pochopit, které funkce jsou často používány, a zlepšit je. Dále můžeme odhadnout, jak dlouho musí být podporována starší technologie a kdy můžeme například zavést povinně novější verzi operačního systému, aniž by to ovlivnilo (příliš mnoho) uživatelů. - zlepšit aplikaci + To zahrnuje hardwarové a softwarové informace o vašem telefonu, nastavení aplikace elektronického receptu a rozsah použití, ale nikdy údaje o vás nebo vašem zdraví. + Údaje budou společnosti gematik GmbH zpřístupněny pouze zpracovatelem údajů a budou vymazány nejpozději po 180 dnech. Analýzu můžete kdykoli deaktivovat v nabídce aplikace. + Tato data nám umožňují pochopit, které funkce jsou často používané, a zlepšit je. Dokážeme také odhadnout, jak dlouho je potřeba podporovat starší technologii a kdy můžeme například zavést povinně novější verzi operačního systému, aniž bychom ovlivnili (příliš mnoho) uživatelů. + Vylepšete aplikaci Anonymní analýza zůstává zakázána %s Děkujeme za vaši podporu! Registrovat Pro stažení receptů se prosím identifikujte. Poznámka pro lékárny: Kontaktní údaje a informace o lékárnách získáváme z mein-apothekenportal.de německého svazu lékáren Zjistili jste chybu nebo byste chtěli údaje opravit? Zjistěte více - lékárny + Lékárny Bohužel to nefungovalo \uD83D\uDE15 Zkuste to znovu. Zadejte heslo Dále Přístupnost Zvětšení - Umožňuje zvětšit aplikaci sevřením nebo roztažením prstů (přiblížení sevřením). + Umožňuje zvětšit aplikaci přiblížením sevřením. Heslo Zabezpečte svá data heslem dle vašeho výběru. Heslo - Uložit do počítače - zobrazit heslo + Uložit + Zobrazit heslo Zopakovat heslo Doporučení: %s - napsat mail - Když odešlete svou zprávu, budou odeslány následující informace o použitém hardwaru a operačním systému: + Napište email + Když odešlete zprávu, přenesou se následující informace o použitém hardwaru a operačním systému: Uplatnit pouze na místě Do této lékárny zatím nelze zasílat elektronické recepty. Momentálně otevřeno @@ -177,7 +177,7 @@ Rozuměl Opakovaná hesla se shodují Chyba 20 10 76631 - Váš certifikát zdravotní karty je neplatný. Vypršela platnost vaší karty? Kontaktujte prosím svou zdravotní pojišťovnu. + Váš certifikát zdravotní karty je neplatný. Možná vypršela platnost vaší karty? Kontaktujte prosím svou zdravotní pojišťovnu. Neúspěšné pokusy o přihlášení Bylo zjištěno %s neúspěšných pokusů o přihlášení. @@ -186,9 +186,9 @@ Bylo zjištěno %s neúspěšných pokusů o přihlášení. Vyberte nejlepší zálohu zařízení - Může to být otisk prstu, vzor přejetí nebo podobně - žetony - přístupový token + Může to být otisk prstu, vzor přejetí nebo něco podobného + Tokeny + Přístupové tokeny SSO tokeny Není k dispozici žádný přístupový token není k dispozici žádný token jednotného přihlášení @@ -199,26 +199,26 @@ žádné připojení k serveru Zkuste to znovu za několik minut Znovu načíst - ukázat žetony + Zobrazit žetony Jak chcete aplikaci zabezpečit? Oznámení Pro toto zařízení nebylo nastaveno žádné zálohování zařízení - Doporučujeme, abyste svá zdravotní data navíc chránili zabezpečením zařízení, jako je přístupový kód nebo biometrie. - Toto upozornění v budoucnu již nezobrazujte. + Doporučujeme, abyste své lékařské informace navíc chránili zabezpečením zařízení, jako je kód nebo biometrie. + V budoucnu toto upozornění již nezobrazujte. Připojení se nezdařilo. Síťové připojení se nepodařilo navázat. Komunikace se serverem selhala: stavový kód %s . - Selhala komunikace se serverem: Zkontrolujte připojení k internetu a nastavení času a data. + Komunikace se serverem selhala: Zkontrolujte připojení k internetu a nastavení času a data. Varování Vaše zařízení může mít snížené zabezpečení - To může být způsobeno například manipulovanými zařízeními nebo aktivovaným vývojářským režimem. Z bezpečnostních důvodů nedoporučujeme používat aplikaci na jailbreaknutých zařízeních. - Uznávám zvýšené riziko a přesto chci pokračovat. + To může být způsobeno například manipulovanými zařízeními nebo zapnutím vývojářského režimu. Z bezpečnostních důvodů doporučujeme nepoužívat aplikaci na jailbreaknutých zařízeních. + Uznávám zvýšené riziko a přesto bych rád pokračoval. Proč jsou zařízení s přístupem root potenciálním bezpečnostním rizikem? Zjistěte více https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html - Jméno profilu + Název profilu Zadejte prosím název nového profilu. - jméno profilu - profily + Jméno profilu + Profily Jak rozpoznat zdravotní kartu s podporou NFC Prostřednictvím této aplikace není možný žádný kontakt Ke kontaktování vaší pojišťovny použijte obvyklé kanály. @@ -226,12 +226,12 @@ Pouze PIN Registrace v aplikaci e-recept Pole názvu nesmí být prázdné. - Profil se zadaným názvem již existuje. + Profil se zadaným jménem již existuje. profil %s vybráno barva pozadí - jarní šedá - rosnatka + Jarní šedá + Rosnatka To! Je! Růžový! Strom Modrý měsíc září @@ -239,20 +239,20 @@ Svázané dohromady Naposledy připojeno %s Smazat profil? - Tím se vymažou všechna data profilu na tomto zařízení. Vaše recepty ve zdravotnické síti zůstanou nedotčeny. - Uhasit - Přerušit + Tím smažete všechna data z profilu na tomto zařízení. Vaše recepty ve zdravotnické síti budou zachovány. + Vymazat + zrušení smazat profil Chcete smazat poslední profil. Aplikace vyžaduje alespoň jeden profil. Zadejte prosím název nového profilu. Chyba 20 10 76831 - Adresář zdravotních karet nebyl dostupný. Prosím zkuste to znovu. - Odborně ověřené informace o nemocech, ICD kódech ao problematice prevence a péče naleznete na Národním portálu zdraví. - Otevřete Gesund.bund.de + Adresář zdravotní karty nebyl dostupný. Prosím zkuste to znovu. + Odborně ověřené informace o nemocech, ICD kódech a tématech prevence a péče naleznete v Národním portálu zdraví. + Otevřete stránku healthy.bund.de Změnili jsme zásady ochrany osobních údajů Aplikace elektronických receptů se vyvíjela. Kvůli tomu bylo nutné aktualizovat naše zásady ochrany osobních údajů. Otevřete zásady ochrany osobních údajů - To se od %s s změnilo: + Toto se od %s s změnilo: Co se stane, když aplikaci otevřete? Co se stane, když použiji funkci fotoaparátu / čtu recepty s fotoaparátem? Nejsou k dispozici žádné nové recepty @@ -269,17 +269,17 @@ Zobrazit protokoly přístupu Kdo a kdy měl přístup k vašim receptům? Přístupový klíč ke službě předpisu - přístupové protokoly + Přístupové protokoly Žádné protokoly přístupu Zatím nejsou žádné přístupové protokoly. Recept se právě zpracovává a nelze jej smazat Akceptovat Zřejmě to nefungovalo - Jsme si vědomi, že spojení se zdravotní kartou má svá úskalí. V budoucnu by proto měla být registrace možná i prostřednictvím již ověřené aplikace zdravotního pojištění. \n\n Pracujeme také na tom, aby bylo možné recepty uplatňovat digitálně bez registrace. \n\n Všimli jste si během tohoto procesu něčeho, o co byste se s námi chtěli podělit? Napište nám, rádi obdržíme i velmi kritickou zpětnou vazbu. + Jsme si vědomi, že spojení se zdravotní kartou má svá úskalí. V budoucnu by měla být registrace možná i přes již ověřenou aplikaci zdravotního pojištění. \n\n Pracujeme také na zajištění toho, aby bylo možné recepty uplatňovat digitálně bez registrace. \n\n Všimli jste si během tohoto procesu něčeho, o co byste se s námi chtěli podělit? Napište nám, rádi také obdržíme velmi kritickou zpětnou vazbu. Tipy pro připojení Zlepšete pevnost spojení V případě potřeby sejměte ochranný kryt. - Pokud zařízení vibruje a následně přeruší spojení, hledejte optimální polohu v malém okruhu. + Pokud zařízení vibruje a spojení se poté přeruší, hledejte optimální polohu v malém okruhu. Pohybujte zařízením po mapě velmi pomalu. Umístěte zařízení přímo na kartu. Chcete-li to provést, položte zdravotní kartu na rovný povrch (např. @@ -290,18 +290,18 @@ Další tip Dále Zavřít - Vyzkoušet + Snaž se napište nám - Licence na vyhledávání lékáren - vykoupit - Naskenovaný předpis + Licenční vyhledávání lékárny + Vykoupit + Naskenovaný recept Naskenováno %s Označeno jako uplatněné %s Jak chcete pokračovat? Objednat Brzy dostupný - Rezervujte si ji hned k vyzvednutí nebo si ji nechte doručit kurýrní službou nebo poštou - Uschovejte pro pozdější objednávku + Rezervujte si ji nyní k vyzvednutí nebo si ji nechte doručit kurýrem nebo poštou + Uschovejte pro pozdější objednání Uložte recepty do zařízení Pokračujte %s receptem @@ -309,42 +309,42 @@ Pokračujte s %s recepty - Nepodařilo se připojit zdravotní kartu - Aktuální profil je již připojen k jiné zdravotní kartě (číslo zdravotního pojištění %s ). - Vaše zdravotní karta je již připojena k jinému profilu. Přepnout na profil %s . - Uložit do počítače - kontaktní údaje a adresu + Připojení zdravotní karty se nezdařilo + Aktuální profil je již propojen s jinou zdravotní kartou (číslo zdravotního pojištění %s ). + Vaše zdravotní karta je již propojena s jiným profilem. Přejít na profil %s . + Uložit + Kontaktní údaje a adresa Kontakt - telefonní číslo - Uveďte prosím telefonní číslo pro kontakt. + Telefonní číslo + Uveďte prosím telefonní číslo, na kterém nás můžete kontaktovat. E-mailová adresa (volitelné) doručovací adresa - jméno a příjmení - Pro kontaktní účely prosím zadejte jméno a příjmení. + Jméno a příjmení + Chcete-li nás kontaktovat, uveďte prosím jméno a příjmení. Ulice a číslo domu - Zadejte prosím ulici a číslo domu, abychom vás mohli kontaktovat. + Uveďte prosím ulici a číslo domu, abyste nás mohli kontaktovat. Další adresa (volitelné) Pokyny k doručení (volitelné) Jsou vyžadovány další kontaktní údaje Zrušit změny? vyřadit Pro vyhledávání používá adresář lékáren zeměpisné souřadnice, které byly určeny pomocí OpenStreetMap. Děkujeme projektu za tuto pomoc. - © OpenStreetMap ( %s ) + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Ochrana osobních údajů a použití + Ochrana dat a použití Dále PIN jste obdrželi v dopise od vaší zdravotní pojišťovny. Nebyl přijat žádný PIN PIN kód - Zkontrolujte připojení k internetu a nastavení času a data na vašem zařízení. + Zkontrolujte připojení zařízení k internetu a nastavení času a data. Pro přihlášení stiskněte „Odemknout“. - uzamčen? Ověřte prosím své biometrické údaje na tomto zařízení. + Uzamčen? Ověřte prosím své biometrické údaje na tomto zařízení. Zapomenuté heslo? Smažte aplikaci a poté ji znovu nainstalujte. Proč tomu tak je, můžete zjistit v našem %s . oblast pomoci - velikost balení a jednotka + Velikost balení a jednotka aktivní složka Množství účinné látky - označení šarže + Název šarže Exp kategorie Vakcína @@ -355,44 +355,44 @@ Zadejte heslo Heslo musí mít alespoň osm znaků Síla hesla není dostatečná - Dostatečná síla hesla + Síla hesla je dostatečná Heslo je viditelné Heslo není viditelné biometrie Heslo - čekání na odpověď + Čekání na odpověď Žádné recepty - Momentálně nemáte žádné recepty s možností uplatnění. + Aktuálně nemáte žádné recepty s možností uplatnění. Aktualizovat Automatické odhlášení - Z bezpečnostních důvodů je spojení se serverem receptů ukončeno po 12 hodinách. Znovu se připojte, abyste získali aktuální recepty. + Z bezpečnostních důvodů se spojení se serverem receptů po 12 hodinách přeruší. Znovu se připojte, abyste získali aktuální recepty. Připojit - Obdrželi jste papírovou kopii? + Obdrželi jste papírový výtisk? Přidejte recepty do svého seznamu klepnutím na tlačítko skenování v pravém horním rohu. Naskenujte papírový výtisk - Pro automatický příjem receptů musíte být přihlášeni. + Chcete-li dostávat recepty automaticky, musíte být přihlášeni. Registrovat Žádné vyplacené recepty Zde jsou zobrazeny vaše uplatněné recepty. Z důvodu ochrany dat budou vaše recepty po 100 dnech smazány ze serveru receptů. Žádné vyplacené recepty Zde jsou zobrazeny vaše uplatněné recepty. Přidejte recepty pomocí skenování a začněte uplatňovat. - správa zařízení + Správa zařízení Připojená zařízení Registrováno od %s (toto zařízení) Registrováno od %s - Z bezpečnostních důvodů je spojení se serverem receptů ukončeno po 12 hodinách. K opětovnému připojení potřebujete zdravotní kartu a PIN pro každý proces připojení. + Z bezpečnostních důvodů se spojení se serverem receptů po 12 hodinách přeruší. Pro opětovné připojení budete potřebovat zdravotní kartu a PIN pro každý proces připojení. PIN kód - Zadejte svůj PIN (zdravotní kartu). + Zadejte PIN (zdravotní karta). Dále Registrovat Připojená zařízení - odebrat zařízení? - Přerušit - Odebráno + Odebrat zařízení? + zrušení + Odstranit Odebrat toto zařízení? Chcete odebrat %s ? Pokud odeberete %s , bude připojení k receptovému serveru trvale přerušeno nejpozději do 12 hodin. - Zařízení se načítají... + Zařízení se načítají… Žádná zařízení K této zdravotní kartě nejsou připojena žádná zařízení. Zkus to znovu @@ -401,32 +401,32 @@ www např.… Žádné internetové připojení. Léky a obvazy - narkotika - Výdej léků na předpis dle § 4 AMVV + Narkotika + Výdej léků na předpis podle § 4 AMVV Potřebuješ pomoc? Dali jsme pro vás dohromady několik tipů, jak vyřešit nejčastější problémy. Spusťte tipy pro připojení - odemknout + Odemknout karta zablokována Kód PIN byl třikrát zadán nesprávně. Vaše karta byla proto z bezpečnostních důvodů zablokována. - odemknout kartu + Odemknout kartu Zadejte PUK Spolu s PINem jste od své pojišťovny obdrželi 8místný PUK. Vyberte nový PIN - Své nové osobní identifikační číslo (PIN) si můžete zvolit sami (6 až 8 číslic). - Pamatujete si PIN? - Poznamenejte si svůj PIN a uschovejte jej na bezpečném místě. - Přerušit + Sami si můžete zvolit své nové osobní identifikační číslo (PIN) (6 až 8 číslic). + Zapamatovali jste si svůj PIN? + Zapište si prosím svůj PIN a uschovejte jej na bezpečném místě. + zrušení OK - Odemknutí není možné - Dosáhli jste maximálního počtu odemknutí karty pomocí tohoto PUK nebo jej opakovaně zadali nesprávně. Kontaktujte prosím svou pojišťovnu. + Odblokování není možné + Dosáhli jste maximálního počtu odemknutí karty pomocí tohoto PUK nebo jste jej opakovaně zadali nesprávně. Kontaktujte prosím svou pojišťovnu. Jeden PUK můžete použít až pro 10 odemknutí. - karta odblokována + Karta odblokována Co potřebuješ: - svou zdravotní kartu + Vaše zdravotní karta PUK vašeho zdravotního průkazu Dále - zdravotní průkaz + Zdravotní průkaz Objednejte si PIN nebo kartu Registrovat Jak se chcete přihlásit? @@ -436,71 +436,71 @@ Požádejte nyní Nebo: Přihlaste se pomocí %s . Vaše aplikace zdravotního pojištění - "Vaše přístupové číslo najdete v pravém horním rohu své zdravotní karty." + "Své přístupové číslo najdete v pravém horním rohu své zdravotní karty." Moje karta nemá přístupové číslo - Máte ještě %s pokusů, než bude vaše karta zablokována. + Máte %s ještě jeden pokus, než bude vaše karta zablokována. Máte ještě %s pokusů, než bude vaše karta zablokována. - Vložte zdravotní kartu na zadní stranu telefonu + Umístěte zdravotní kartu na zadní stranu telefonu Následující proces může trvat až 30 sekund. Umístěte kartu %s na zadní stranu telefonu. - v pravém horním rohu - v horním středu - vlevo nahoře + v pravé horní oblasti + uprostřed v horní oblasti + v levé horní oblasti ve střední oblasti vpravo střední - uprostřed vlevo + ve střední oblasti vlevo v pravé dolní oblasti - v dolním středu - vlevo dole + uprostřed v dolní oblasti + v levé dolní oblasti Pomoc Odesláno před %s minutami Odesláno %s Právě odesláno - Odesláno v %s hodin + Odesláno v %s čas Nadále není platné - Přihlaste se pomocí aplikace - vybrat pojištění - Nenašli jste, co jste hledali? Tento seznam se neustále rozšiřuje. Registraci se zdravotním průkazem podporuje již každá zdravotní pojišťovna. + Zaregistrujte se pomocí aplikace + Vyberte pojištění + Nenašli jste, co jste hledali? Tento seznam se neustále rozšiřuje. Registraci se zdravotním průkazem již podporuje každá zdravotní pojišťovna. Zpětná vazba z aplikace e-recept - Těšíme se na vaši zpětnou vazbu. Použijte prosím níže uvedený prostor a buďte co nejpřesnější: + Těšíme se na vaši zpětnou vazbu. Použijte prosím následující mezeru a buďte co nejpřesnější: PUK Zavřít Jaká škoda… - Vaše zařízení bohužel nesplňuje minimální požadavky pro přihlášení do aplikace e-recept. Pro bezpečné ověření pomocí zdravotní karty je vyžadován alespoň Android 7 a čip NFC. + Vaše zařízení bohužel nesplňuje minimální požadavky pro registraci do aplikace e-recept. Pro bezpečné ověření pomocí zdravotní karty je vyžadován alespoň Android 7 a čip NFC. Zjistěte více Uložit přihlašovací údaje? - Uložit do počítače + Uložit Nešetřete Oznámení - Z bezpečnostních důvodů je spojení se serverem receptů ukončeno po 12 hodinách. Pro opětovné připojení potřebujete zdravotní kartu a PIN pro každý proces připojení. + Z bezpečnostních důvodů se spojení se serverem receptů po 12 hodinách přeruší. Pro opětovné připojení budete potřebovat zdravotní kartu a PIN pro každý proces připojení. Nastavte si biometrické zabezpečení - Uložení přístupových údajů není možné. Předem si na svém zařízení nastavte biometrické zabezpečení (např. otisk prstu). - Přerušit - Nápady + Není možné ukládat přístupové údaje. Předtím si na svém zařízení nastavte biometrické zabezpečení (např. otisk prstu). + zrušení + Nastavení Oznámení Akceptovat Zabezpečení vašich údajů o předpisech - \\"Tato aplikace používá nejbezpečnější biometrický senzor poskytovaný vaším zařízením k ukládání vašich přihlašovacích údajů v zabezpečené oblasti paměti zařízení.\" - Biometrické zabezpečení vašich přístupových údajů vám umožňuje v budoucnu otevřít tuto aplikaci bez zadání PIN a zdravotní karty a prohlížet, vyvolávat, uplatňovat nebo mazat recepty. - Zajistěte prosím, aby osoby, se kterými můžete toto zařízení sdílet a jejichž biometrické charakteristiky mohou být v tomto zařízení uloženy, měly také přístup k vašim předpisům. + \\"Tato aplikace používá nejbezpečnější biometrický senzor poskytovaný vaším zařízením k zabezpečení vašich přihlašovacích údajů v chráněné oblasti úložiště zařízení.\" + Biometrické zabezpečení vašich přístupových údajů vám umožňuje otevřít tuto aplikaci v budoucnu, prohlížet, načítat, uplatňovat nebo mazat recepty bez zdravotní karty a zadání vašeho PIN. + Zajistěte prosím, aby lidé, se kterými můžete toto zařízení sdílet a jejichž biometrické charakteristiky mohou být v tomto zařízení uloženy, měli také přístup k vašim předpisům. což se bohužel nepovedlo - Ověření pomocí aplikace zdravotního pojištění se nezdařilo. + Ověření pomocí aplikace zdravotního pojištění nebylo úspěšné. Platnost vypršela %s Recept byl již smazán ze serveru - Opravte svůj vstup nebo zrušte změny + Opravte prosím zadání nebo zrušte změny Opravit - pojištěné údaje + Údaje o pojištěné osobě Příjmení Pojištění cislo pojistence - přístupové číslo karty + Přístupové číslo karty Registrovat - Zrušit registraci - Uložit do počítače + Odhlásit se + Uložit Změna Upravit profilový obrázek Dále @@ -513,10 +513,10 @@ spojení ztraceno Chcete se nyní připojit k serveru receptů? Žádné žetony - Token obdržíte, když se přihlásíte do služby předpisu.\n - objednávky + Token obdržíte, když se přihlásíte do služby předpisu.\n + Objednávky Vyberte požadovaný PIN - odemknout kartu + Odemknout kartu Vyberte PIN Opakujte PIN Záznamy se od sebe liší. @@ -525,13 +525,13 @@ Právě teď V %s hodin Nákupní košík je připraven - Recept byl přidán do vašeho nákupního košíku. Pro dokončení objednávky prosím přejděte na web lékárny. + Recept byl přidán do vašeho košíku. Pro dokončení objednávky přejděte na webovou stránku lékárny. Otevřete nákupní košík - Ukažte tento kód pro vyzvednutí v lékárně. - Přijměte kód pro vyzvednutí + Ukažte tento kód sbírky v lékárně. + Kód vyzvednutí přijat Zprávu nelze zobrazit Kontaktujte prosím svou lékárnu ( %s ). - Zobrazit odkaz na košík + Zobrazit odkaz na nákupní košík Zobrazit kód vyzvednutí Ukažte zprávu %s v %s hodin @@ -539,7 +539,7 @@ Přehled objednávek Nový Chod - Objednat + Objednávka Zdarma pro volajícího. Servisní časy: Po - Pá 8:00 - 20:00 kromě státních svátků LÉKÁRNA Vyberte požadovaný PIN @@ -547,42 +547,42 @@ Momentálně otevřené a blízko mě Filtrovat podle … spustit vyhledávání - přímé zadání - lékárny - telefonní číslo (nepovinné) + Přímé zadání + Lékárny + telefonní číslo (volitelné) Hledejte podle jména nebo adresy Žádné platné informace o lékárně O této lékárně nebyly nalezeny žádné aktuální informace. Záznam pro tuto lékárnu bude smazán. OK Adresář lékáren není k dispozici - V současné době nelze získat žádné aktuální informace o této lékárně. Zkontrolujte prosím své internetové připojení. - Přerušit + V současné době nejsou dostupné žádné aktuální informace o této lékárně. Zkontrolujte prosím své internetové připojení. + zrušení Zkus to znovu Zachraňte životní prostředí Přihlášení není možné - Zdá se, že vaše biometrické přihlašovací údaje se změnily. Zaregistrujte se prosím znovu pomocí své zdravotní karty. - Přerušit + Zdá se, že vaše biometrické přihlašovací vlastnosti se změnily. Přihlaste se prosím znovu pomocí své zdravotní karty. + zrušení Registrovat - profil 1 + Profil 1 Blízko mě Splatný později Lze uplatnit od %s - vylepšení produktu + Vylepšení produktu Anonymní analýza - Pomozte nám tuto aplikaci vylepšit. Všechny údaje o používání jsou shromažďovány anonymně a slouží pouze ke zlepšení uživatelské zkušenosti. - zabezpečení zařízení + Pomozte nám tuto aplikaci vylepšit. Veškeré údaje o používání jsou shromažďovány anonymně a slouží výhradně ke zlepšení uživatelské zkušenosti. + Zabezpečení zařízení osobní nastavení Přístupnost - vylepšení produktu + Vylepšení produktu Přidán recept Recept je již k dispozici Při importu došlo k chybě - Uhasit - Naskenovaný předpis - Náhrada možná - Zapomněli jste PIN + Vymazat + Naskenovaný recept + Možnost přípravy na výměnu + Zapomenutý PIN - %s recept + %s Recept %s Recepty @@ -598,51 +598,50 @@ Pokračovat Akceptovat Tato aplikace používá nejbezpečnější metodu poskytovanou vaším zařízením. - Uložit do počítače + Uložit Vybrat lék - jméno výrobku + Jméno výrobku Ano Ne dávkování datum vydání Tento předpis vám bude vyplacen jako součást léčby. Nespecifikováno - doplatek + Doplatek lék - Dodací listy + Pokyny k podání Způsobilé podle BVG - alternativní příprava - název receptu + Alternativní příprava + Název receptu Obal - crafting instrukce + Návod na výrobu Popis dána vydáno dne: aktivní složka - předepsané + Předepsané Dostávat Co je to přímé zadání? - V případě přímého doporučení se recept z praxe nebo nemocnice vyplácí přímo v lékárně. Pojištěnci nemusejí činit žádnou akci a nemohou zasahovat do procesu odkupu. \n\n Přímá doporučení jsou uvedena v aplikaci e-recept, aby pro vás byla vaše léčba transparentnější. - poplatek za pohotovostní službu - Někdy je potřeba spěchat. Některé recepty lze uplatnit bez dodatečného placení poplatku za pohotovostní službu, například v noci nebo o státních svátcích. + S přímým doporučením je recept z praxe nebo nemocnice vyplněn přímo v lékárně. Pojištěnci nemusejí činit žádnou akci a nemohou zasahovat do procesu odkupu. \n\n Přímá doporučení jsou uvedena v aplikaci e-recept, aby pro vás byla vaše léčba transparentnější. + Poplatek za pohotovostní službu + Někdy je nutný spěch. Některé recepty lze vyplnit bez doplatku poplatku za pohotovostní službu, například v noci nebo o svátcích. Léky podléhající spoluúčasti - Osvobozeno od spoluúčasti - Osoby se zákonným zdravotním pojištěním musí za léky na předpis zaplatit spoluúčast až deset eur. \n\n Výše doplatku závisí na ceně vašeho léku. Léky, které stojí méně než 5 EUR, si musíte zaplatit sami.\n Za léky, které jsou dražší, musíte zaplatit deset procent z ceny, minimálně však 5 € a maximálně 10 €. \n\n Děti a mládež do 18 let jsou obecně od spoluúčasti osvobozeni. \n\n Pokud vaše roční náklady na léky překročí váš finanční limit, můžete být od spoluúčasti osvobozeni. Promluvte si o tom se svou zdravotní pojišťovnou. - Jste osvobozeni od spoluúčasti tohoto léku. Náklady na léky uhradí vaše zdravotní pojišťovna. + Osvobozeno od doplatku + Osoby se zákonným zdravotním pojištěním musí za léky na předpis doplatit až deset eur. \n\n Výše doplatku závisí na ceně vašeho léku. Léky, které stojí méně než 5 EUR, si musíte zaplatit sami.\n Za léky, které jsou dražší, musíte zaplatit deset procent z ceny, minimálně však 5 € a maximálně 10 €. \n\n Děti a mládež do 18 let jsou obecně osvobozeni od doplatku. \n\n Pokud vaše roční náklady na léky překročí limit finanční zátěže, můžete být od spoluúčasti osvobozeni. Promluvte si o tom se svou zdravotní pojišťovnou. + Jste osvobozeni od placení spoluúčasti za tento lék. Náklady na léky uhradí vaše zdravotní pojišťovna. Jak dlouho je tento předpis platný? Během tohoto období můžete svůj recept uplatnit v jakékoli lékárně s maximálním doplatkem 10 EUR. - Náhrada možná - Vzhledem k zákonným požadavkům vaší zdravotní pojišťovny vám může být poskytnuta alternativa se stejnou účinnou látkou. \n\n Léky mohou vypadat a jmenovat se různě, mít různé ceny i výrobce, ale stále obsahují stejnou účinnou látku. Pro účinek léků v organismu je důležitá především samotná účinná látka a dávkování. Pacienti v lékárně často dostanou jiný lék, než jaký jim lékař předepsal na receptu – pokud jsou léky srovnatelné. Pro změnu mohou existovat terapeutické a ekonomické důvody. - Naskenovaný předpis - Z bezpečnostních důvodů nesmí receptury importované z papírového výtisku zobrazovat žádné osobní nebo lékařské údaje. \n\n Přihlaste se do této aplikace pomocí zdravotní karty nebo pojišťovací aplikace a zobrazte všechny informace obsažené v předpisu. + Možnost přípravy na výměnu + Na základě zákonných požadavků vaší zdravotní pojišťovny vám může být poskytnuta alternativa se stejnou účinnou látkou. \n\n Léky mohou vypadat a jmenovat se různě, mít různé ceny a výrobce, ale stále obsahují stejnou účinnou látku. Pro účinek léků v organismu je rozhodující samotná účinná látka a dávkování. Pacienti často dostanou v lékárně jiný lék, než jaký předepsal lékař – pokud jsou léky srovnatelné. Pro změnu mohou existovat terapeutické a ekonomické důvody. + Naskenovaný recept + Předpisy importované z tištěné kopie nemohou z bezpečnostních důvodů zobrazovat osobní nebo lékařské informace. \n\n Přihlaste se do této aplikace pomocí zdravotní karty nebo pojišťovací aplikace a zobrazte všechny informace obsažené v předpisu. Recept nesprávný Tento předpis byl vystaven chybně. - Naskenovaný předpis - poplatek za pohotovostní službu + Poplatek za pohotovostní službu Dávkování dle písemného návodu telefon - místo + webová stránka Pošta Třídění podle vzdálenosti není možné. OK @@ -650,68 +649,68 @@ Zadán nesprávný PIN Aktuální PIN vaší zdravotní karty karta zablokována - Odblokujte kartu v Nastavení > Odblokovat kartu. + Odemkněte kartu v Nastavení > Odemknout kartu. Z bezpečnostních důvodů zadejte svůj aktuální PIN. - Zapomněli jste PIN + Zapomenutý PIN Nesprávný recept lék Zdá se, že se při vytváření vašeho receptu něco pokazilo. Nahlásit chybu? Zpráva Nepřihlášen Registrován u - zdravotní průkaz + Zdravotní průkaz biometrie Nepřihlášen - Váš názor nás zajímá. Věnujte prosím pět minut odpovědi na náš průzkum. Děkuji předem. - varovné upozornění + Váš názor nás zajímá. Věnujte prosím pět minut vyplnění našeho průzkumu. Předem moc děkuji. + Upozornění Lékárna přidána do oblíbených - Lékárna byla odebrána z oblíbených + Lékárna odstraněna z oblíbených Moje lékárny Síla hesla velmi dobrá - Operace zápisu se nezdařila + Operace zápisu nebyla úspěšná PIN se nepodařilo uložit Zpráva Přiřadit PIN Porušeno pravidlo přístupu Nemáte oprávnění pro přístup k adresáři map. Přiřaďte svůj vlastní pin - Karta je zabezpečena PIN od vaší zdravotní pojišťovny (PIN pro přepravu), přidělte si prosím vlastní PIN. + Karta je zabezpečena PIN od vaší zdravotní pojišťovny (PIN pro přepravu), zadejte prosím vlastní PIN. Heslo nenalezeno Na vaší kartě není uloženo žádné heslo. Byli jste odhlášeni - Chcete-li aktualizovat své recepty, znovu se přihlaste. - číslo účinné látky + Chcete-li aktualizovat své recepty, přihlaste se znovu. + Číslo účinné látky síla a jednota Uplatněno před %s minutami Uplatněno %s Vykoupeno právě teď Uplatněno v %s hodin - objednávky - Tento předpis byl pro vás vykoupen jako součást léčby. - poplatek za pohotovostní službu - Tento recept nelze vyplnit v noci v lékárně bez doplatku poplatku za pohotovostní službu. + Objednávky + Tento předpis byl vyplněn jako součást léčby pro vás. + Poplatek za pohotovostní službu + Tento recept nelze vyplnit v lékárně v noci bez doplacení poplatku za pohotovostní službu. Hledej tady - Nápady - Sdílejte polohu v nastavení. + Nastavení + Sdílejte polohu v Nastavení. Blízko mě - Podržením upravíte název. + Stisknutím a podržením upravíte jméno. Zadejte nový název profilu. - Abyste mohli dostávat digitální recepty z vaší praxe, musíte být přihlášeni. - Přijímat recepty digitálně? - Pro obnovení přetáhněte obrazovku dolů. + Chcete-li přijímat recepty digitálně ze své praxe, musíte být přihlášeni. + Dostávat recepty digitálně? + Pro obnovení stáhněte obrazovku dolů. Žádné recepty Přidejte recepty pomocí tlačítka + v pravém horním rohu. Registrovat - archiv receptů + Archiv receptů Možná později Registrovat Upravit profilový obrázek - archiv receptů + Archiv receptů Napište jméno - Uložit do počítače + Uložit Moje objednávka Příjemce: in - recepty + Recepty LÉKÁRNA Poslat Změna @@ -719,22 +718,22 @@ Doručení kurýrem Doručení poštou %s Recepty - Uplatnění není možné + Není možné vykoupit Jeden nebo více receptů nebylo možné uplatnit. - Není vybrán žádný recept + Nebyl zvolen žádný recept Pro uplatnění receptů je nutné vybrat alespoň jeden recept. - Přidejte kontaktní informace + Přidejte kontaktní údaje Změna - Žádný předpis + Žádný recept Momentálně nemáte žádné recepty s možností uplatnění sbírka poslíček náklad - vybrat recepty + Vyberte si recepty Klepnutím sem naskenujete recepty Dlouhým stisknutím upravíte jména - Přidejte další profily, např. pro vaše děti nebo rodiče - Klepnutím na displej přeskočíte zobrazený tip nástroje. + Přidejte další profily, například pro vaše děti nebo rodiče + Klepnutím na displej přeskočíte zobrazený popis. Jak vykoupit? Jak byste chtěli dostávat své léky? Uplatnit přímo @@ -742,10 +741,10 @@ Objednat Rezervujte nebo nechte dovézt Připraveno - kolektivní kód - jednotlivé kódy + Kód sbírky + Jednotlivé kódy - Máte %s předpis. + Máte %s recept. Máte %s receptů. @@ -759,65 +758,65 @@ Oznámení Tato aplikace používá k rozpoznání kódů software od Googlu. Zjistěte více - O skeneru kódů receptů + Informace o skeneru kódů receptů Jaká data obsahuje kód receptury? - Kód receptury obsahuje pouze identifikátor receptury. To umožňuje, aby byl recept nalezen na předpisové službě v digitální zdravotnické síti. Kód předpisu neobsahuje žádné údaje o vás nebo vašich lécích. - Takže jen s kódem receptu nemůže nikdo nic dělat? - Opravit. Údaje o receptu je nutné stáhnout ze služby předpisu. To vyžaduje bezpečné přihlášení. + Kód receptury obsahuje pouze identifikátor receptury. To znamená, že předpis lze nalézt na předpisové službě v síti digitálního zdraví. Kód předpisu neobsahuje žádné informace o vás nebo vašich lécích. + Takže jen s kódem receptu nikdo nic nezmůže? + Opravit. Údaje o receptu je nutné stáhnout ze služby předpisu. K tomu je vyžadováno bezpečné přihlášení. Kdo se může zaregistrovat ke službě receptů? - Registraci k preskripční službě v síti digitálního zdraví mohou mít pojištěnci, lékárny, lékařské ordinace a nemocnice. + Registrace do služby preskripce v síti digitálního zdraví je možná pro pojištěnce, lékárny, praxe a nemocnice. Proč aplikace pro elektronický předpis používá funkce Google? - Google nabízí funkce, které lze snadno zabudovat do aplikací a které společnost Google neustále vyvíjí a aktualizuje. To zajišťuje, že funkce fungují na mnoha různých koncových zařízeních a mohou být bezpečně provozovány. Aplikace využívá funkci ke zlepšení funkcí fotoaparátu a skenování pro zařízení Android (Google ML Kit). - Jak funguje vylepšení skenování sady Google ML Kit? + Google nabízí funkce, které lze snadno integrovat do aplikací a které Google neustále vyvíjí a aktualizuje. To zajišťuje, že funkce fungují na mnoha různých zařízeních a lze je bezpečně ovládat. Aplikace využívá funkci pro vylepšení funkcí fotoaparátu a skenování pro zařízení Android (Google ML Kit). + Jak funguje vylepšení skenování s Google ML Kit? Google ML Kit pomáhá optimalizovat obraz snímaný kamerou tak, aby bylo možné číst kódy receptů i za zhoršených světelných podmínek nebo se staršími modely fotoaparátů. - Budou údaje o předpisu nebo mém léku předány společnosti Google? - Ne. Načtený kód receptu se uloží přímo v aplikaci. Nebudou předány společnosti Google. Údaje o předpisech nejsou uloženy v kódu, pouze v digitální zdravotnické síti. Odtud jsou odeslány do aplikace. Google nemá přístup k digitální zdravotnické síti. + Budou údaje o předpisu nebo mém léku sdíleny se společností Google? + Ne. Načtený kód receptu se uloží přímo v aplikaci. Nebude sdílen se společností Google. Údaje o předpisech nejsou uloženy v kódu, ale pouze v digitální zdravotnické síti. Odtud jsou přenášeny do aplikace. Google nemá přístup k digitální zdravotnické síti. Jaká data zpracovává Google při používání ML Kit? - Google má přístup pouze k technickým informacím o použitém koncovém zařízení a obecném použití doplňkové funkce (např. chybovost, nastavení fotoaparátu), aby to mohl statisticky zaznamenat a tím doplňkovou funkci vylepšit. Když přistupujete, Google dočasně zaznamená IP adresu vašeho koncového zařízení. Informace o vás a obsah receptu nebudou společností Google zaznamenány. + Google získává přístup pouze k technickým informacím o používaném zařízení a obecném použití doplňkové funkce (např. chybovost, nastavení fotoaparátu), aby to mohl statisticky zaznamenat a zlepšit tak doplňkovou funkci. Při přístupu Google dočasně zaznamená IP adresu vašeho zařízení. Informace o vás a obsah receptu Google nezaznamenává. Je používání Google ML Kit dobrovolné? - Ano. Sada ML Kit je však zabudována do skeneru kódu receptu ve verzi aplikace pro elektronický předpis pro Android. Pokud používáte čtečku receptových kódů na zařízení Android, vždy se také použije funkce ML Kit. Můžete se však obejít bez použití skeneru kódů receptů. Vaše recepty lze také načíst do aplikace, pokud se zaregistrujete v digitální zdravotní síti pomocí elektronické zdravotní karty nebo prostřednictvím aplikace zdravotního pojištění. + Ano. ML Kit je však zabudován do skeneru kódu receptu ve verzi aplikace pro elektronické recepty pro Android. Pokud používáte skener kódů receptur na zařízení Android, vždy se použije funkce ML Kit. Skener kódu receptu se však můžete vyhnout. Vaše recepty lze také načíst do aplikace, pokud se přihlásíte do digitální zdravotní sítě pomocí elektronické zdravotní karty nebo prostřednictvím aplikace zdravotního pojištění. Mohu vidět, kdo si prohlížel mé recepty? Ano. Veškerý přístup k vašim datům je plně přihlášen do digitální zdravotnické sítě. V aplikaci elektronického receptu můžete vidět, kdo měl přístup k vašim datům. - Na koho se mohu obrátit, pokud mám dotazy k aplikaci nebo elektronickému receptu? + Kam se mohu obrátit, pokud mám dotazy k aplikaci nebo elektronickému receptu? Podrobné informace naleznete v prohlášení o ochraně údajů. Předepsaný počet balení Žádné recepty K tomu potřebujete splatné recepty. - vybrat pojištění + Vyberte pojištění Hledejte pojištění - Přerušit + zrušení O co byste se chtěli ucházet? Pro tuto aplikaci potřebujete kartu a související PIN. Jak byste chtěli kontaktovat svou pojišťovnu? Vaše pojišťovna nabízí následující možnosti kontaktu - Vaše pojišťovna nabízí následující možnosti kontaktu + Vaše pojišťovna nabízí následující možnost kontaktu Zavřít PIN zadaný nesprávně. Nesprávně zadané přístupové číslo PUK zadaný nesprávně. výdajové doklady - Zobrazit účtenky na výdaje + Prohlédněte si účtenky o nákladech výdajové doklady Chcete-li přijímat potvrzení o výdajích, musíte být připojeni k serveru. Připojit - Žádné výdajové doklady + Žádné potvrzení o nákladech Deaktivovat - Přerušit - zakázat funkci - Tímto smažete všechny účtenky z tohoto zařízení a ze serveru. - Přijímejte stvrzenky o výdajích + zrušení + Deaktivovat funkci + Tím se vymažou všechny účtenky z tohoto zařízení a serveru. + Přijímat potvrzení o nákladech Vaše účtenky jsou také uloženy na receptovém serveru. - Dostávat + Přijato Celkem: %s %s Vybrat Rozdělit - Uhasit - Uhasit + Vymazat + Vymazat Předložit %s € Celková cena - Tip: Odešlete účtenky o výdajích prostřednictvím aplikace pojištění - Odešlete účtenky snadno prostřednictvím aplikace vaší pojišťovny. V dalším kroku vyberte tuto aplikaci a stiskněte Sdílet. + Tip: Odešlete potvrzení o nákladech prostřednictvím aplikace pojištění + Odešlete účtenky snadno prostřednictvím aplikace vaší pojišťovny. V dalším kroku vyberte tuto aplikaci a stiskněte sdílet. Praxe LÉKÁRNA datum @@ -825,38 +824,64 @@ ID léku Vydáno pro KVNR: %s - Datum narození: %s + Narozen dne: %s OK - Jak předáváte účtenky? - Přeneste se přímo do aplikace vaší pojišťovny / asistenční kanceláře. Chcete-li to provést, vyberte aplikaci na další stránce. + Jak předkládáte podpůrné dokumenty? + Přeneste se přímo do aplikace vaší pojišťovací/dávkové kanceláře. Chcete-li to provést, vyberte aplikaci na další stránce. nebo - Uložte soubor a později jej importujte do portálu pojištění/pomoci. + Uložte soubor a později jej importujte do portálu pojištění/dávek. Článek: %s - Číslo: %s + Počet: %s DPH: %s %% Hrubá cena v EUR: %s Další poplatky - poplatek za pohotovostní službu + Poplatek za pohotovostní službu Poplatek za BTM - T poplatek za předpis - pořizovací náklady + Poplatek za T-recept + Náklady na pořízení Kurýrní služba Celkem v EUR: %s odvod Opravdu smazat? - Soubor bude odstraněn z vašeho zařízení a ze serveru. - Uhasit + Soubor bude smazán z vašeho zařízení a serveru. + Vymazat Vyslán PSČ Umístění Chcete-li nás kontaktovat, zadejte své PSČ. - Při kontaktu s námi prosím uveďte své bydliště. + Chcete-li nás kontaktovat, uveďte prosím své bydliště. Bude pro vás vykoupeno Bylo pro vás vykoupeno Pro použití této služby musíte být přihlášeni. - aplikace pojištění - zdravotní průkaz + Aplikace pojištění + Zdravotní průkaz Je vyžadován přidružený PIN + Lze uplatnit pouze zítra jako samoplátce + Zbývá pouze %s dní na uplatnění jako samoplátce + \nStále lze uplatnit jako samoplátce po dobu %s dnů\n + Platí pouze %s dnů + \nPlatné zbývá %s dnů\n + Platí pouze zítra + Účtují se poplatky + Bere pojištění + Recepty byly úspěšně přeneseny. + Recept nelze zpracovat. Prosím zkuste to znovu. Možná budete muset vybrat jinou lékárnu. + Recept nelze zpracovat. Lékárna hlásí neznámou chybu. V případě potřeby zkuste jinou lékárnu. + Předpis byl lékárnou zamítnut. Předpis může být neplatný nebo vaše doručovací adresa nebo kontaktní údaje mohou být neplatné. + Nelze uplatnit, zkontrolujte prosím připojení k internetu. + Recept byl úspěšně přenesen. Lékárna však hlásí chybu zpracování. Kontaktujte prosím lékárnu. + Předpis byl lékárnou zamítnut. Předpis byl již uplatněn. + Předpis byl lékárnou zamítnut. Recept byl smazán. + Recept se nepodařilo přenést. Zkontrolujte prosím připojení k internetu a zkuste to znovu. + Jeden nebo více receptů nebylo možné přenést. + Chyba při odesílání + Odesláno úspěšně! + Chyba v lékárně + Chyba v lékárně + Kontaktujte lékárnu + Předpis je již uplatněn + Recept smazán + Žádný internet Chcete-li přijímat protokoly o přístupu, musíte být připojeni k serveru. V této lhůtě ještě můžete recept vyplnit v lékárně, ale celou kupní cenu za léky si budete muset zaplatit sami. Případně můžete požádat svou praxi o opětovné vystavení receptu. Připraveno @@ -865,4 +890,13 @@ V aplikaci Nechte si tento kód naskenovat ve své lékárně. Žádost o opravu vyúčtování + lék + Zadejte prosím alespoň 1 znak. + Nebo. Vyzkoušejte aplikaci v demo režimu + Demo režim + Demo režim + Použijte demo režim + Demo režim aktivován + Konec tady + Aktivujte demo režim diff --git a/android/src/main/res/values-da/strings.xml b/app/features/src/main/res/values-da/strings.xml similarity index 63% rename from android/src/main/res/values-da/strings.xml rename to app/features/src/main/res/values-da/strings.xml index 2a6966b2..d989cfc4 100644 --- a/android/src/main/res/values-da/strings.xml +++ b/app/features/src/main/res/values-da/strings.xml @@ -1,17 +1,17 @@ Okay - Afbryde - Vend tilbage + Afbestille + Tilbage rundt om Digital. Hurtig. Sikker. Opgave-id - adgangskode + Adgangskode Vilkår for brug Data beskyttelse - opskrifter - Kameraadgang nægtet - For at bruge scanneren skal du give appen adgang til dit kamera i systemindstillingerne. + Opskrifter + Adgang til kamera nægtet + For at bruge scanneren skal du give appen tilladelse til at få adgang til dit kamera i Systemindstillinger. Fokuser kameraet på en opskriftskode Dette er ikke en gyldig receptkode Denne receptkode er allerede blevet scannet @@ -19,56 +19,56 @@ %s opskrift genkendt %s opskrifter genkendt - Afbryde - kamera lys + Afbestille + Kamera lys Vil du annullere scanningen? Okay Fortryd ikke - Nu sker det + Lad os gå Hvad du har brug for: Indtast kortadgangsnummer indtaste PIN-koden Prøv igen Kunne ikke oprette forbindelse til serveren. - Du har %s forsøg mere, før dit kort blokeres. + Du har %s et forsøg mere, før dit kort blokeres. Du har %s forsøg mere, før dit kort blokeres. - Du finder adgangsnummeret øverst til højre på dit sundhedskort. - Afbryde - Søg efter kort... - Hold sundhedskortet på bagsiden af din enhed. + Du kan finde adgangsnummeret øverst til højre på dit sundhedskort. + Afbestille + Søg på kort... + Hold sundhedskortet mod bagsiden af ​​din enhed. Søger stadig … - Flyt langsomt kortet bag på enheden. + Flyt langsomt kortet på bagsiden af ​​enheden. Tip Enhedsetuier kan gøre det vanskeligt at oprette forbindelse via NFC. - kort genkendt + Kort genkendt Prøv ikke at flytte sundhedskortet. - Sundhedskort fundet. Bevæg dig ikke. + Sundhedskort fundet. Bevæg dig venligst ikke. forbindelse afbrudt - Hold dit sundhedskort på bagsiden af enheden igen + Hold dit sundhedskort mod bagsiden af ​​enheden igen Version: %s - Byg Hash: %s - fejlretningsmenu + Byg hash: %s + Fejlfindingsmenu Åben indtil %s Åbent hele dagen aftryk redaktør gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin - Administrerende direktør: Dr. medicinsk Markus Leyck-Dieken\n Tinglysningsret: byretten Berlin-Charlottenburg\n Handelsregisternummer: HRB 96351\n Moms-identifikationsnummer: DE241843684 + administrerende direktør: Dr. med. Markus Leyck Dieken\n Registreringsret: Berlin-Charlottenburg District Court\n Handelsregisternummer: HRB 96351\n Momsidentifikationsnummer: DE241843684 Ansvarlig for indholdet - dr medicinsk Markus Leyck-Dieken + Dr. med. Markus Leyck Dieken Kontakt Varsel - Vi bestræber os på at bruge et kønsneutralt sprog. Hvis du bemærker fejl, ser vi frem til at høre fra dig via e-mail. + Vi bestræber os på at bruge et ligestillet sprog. Hvis du bemærker fejl, hører vi gerne fra dig via e-mail. Tysklands moderne platform for digital medicin - skrive mail - åben hjemmeside + Skriv e-mail + Åben hjemmeside Velkommen Start registreringen - låse op + Lås op Tilmeld - Afbryde + Afbestille Sikkerhed gyldige aftryk @@ -76,92 +76,92 @@ Vilkår for brug detaljer Markér som indløst - Markér som ikke indløst - doseringsform - pakkestørrelse + Markér som uindløst + Doseringsform + Pakkestørrelse Forsikret person Efternavn adresse fødselsdato - Sygesikring / betalere + Sygesikring/betaler status - forsikringsnummer - Foreskrivende person + Forsikringsnummer + Udskriver Efternavn - Medicinsk specialist + Speciallæge Lægenummer (LANR) institution Efternavn adresse - Erhvervslokalets nummer - telefonnummer - mail addresse - arbejdsulykke - ulykkesdag + Anlægsnummer + Telefon nummer + Email adresse + Arbejdsulykke + Ulykkens dag Ulykkesvirksomheds- eller arbejdsgivernummer Vil du slette denne opskrift permanent? - Sluk - Afbryde + Slet + Afbestille åbningstider internet side Kan kun indløses i dag som selvbetaler Tilmeld Aktiver NFC - Aktiver venligst NFC-funktionen på din enhed for at logge på med dit sundhedskort. + Aktiver venligst din enheds NFC-funktion for at logge på med dit sundhedskort. Aktiver Korrekt - Indløste recepter? + Recepter indløst? Vil du markere recepterne som indløste? Ikke indløst Forløst Åbner kl. %s +49 800 277 377 7 Teknisk hotline - Åbn scanner for opskrifter - Ideer + Åbn scanner for recepter + Indstillinger Undertryk skærmbilleder - Forhindrer visning af et miniaturebillede, når der skiftes app - Tillader du e-opskrifter at analysere din brugsadfærd anonymt? + Forhindrer et eksempelbillede i at blive vist, når der skiftes app + Tillader du E-Recept at analysere din brugsadfærd anonymt? Teknisk information Sikkerhed for dine receptdata Sørg for, at personer, som du må dele denne enhed med, og hvis biometriske karakteristika kan være gemt på denne enhed, også har adgang til dine recepter. afsendelse mislykkedes - Intet e-mail-program opsat + Intet e-mail-program er opsat Ingen resultater Vi kunne ikke finde nogen resultater for denne søgeterm. - Open Source-licenser + Open source-licenser Kontakt - Ring til teknisk hotline + Ring til den tekniske hotline Deltag i undersøgelsen +49 800 277 377 7 Jeg vil gerne hjælpe med at gøre denne app bedre - Dette inkluderer hardware- og softwareoplysninger på din telefon, indstillinger for e-recept-appen og omfanget af brug, men aldrig data om dig personligt eller dit helbred. - Dataene stilles kun til rådighed for gematik GmbH af databehandleren og slettes senest efter 180 dage. Du kan til enhver tid deaktivere analysen igen i appmenuen. - Disse data gør det muligt for os at forstå, hvilke funktioner der bruges hyppigt, og at forbedre dem. Ydermere kan vi vurdere, hvor længe ældre teknologi skal understøttes, og hvornår vi for eksempel kan gøre en nyere version af operativsystemet obligatorisk uden at påvirke (for mange) brugere. - forbedre app + Dette inkluderer hardware- og softwareoplysninger om din telefon, indstillinger for e-recept-appen og omfanget af brug, men aldrig data om dig eller dit helbred. + Dataene stilles kun til rådighed for gematik GmbH af databehandleren og slettes senest efter 180 dage. Du kan til enhver tid deaktivere analysen i appmenuen. + Disse data giver os mulighed for at forstå, hvilke funktioner der ofte bruges og forbedre dem. Vi kan også estimere, hvor længe ældre teknologi skal understøttes, og hvornår vi for eksempel kan gøre en nyere version af operativsystemet obligatorisk uden at påvirke (for mange) brugere. + Forbedre app Anonym analyse forbliver deaktiveret %s Tak for din støtte! Tilmeld Identificer dig selv for at downloade opskrifter. - Bemærkning til apoteker: Vi indhenter kontaktoplysninger og oplysninger om apoteker fra mein-apothekenportal.de fra den tyske apotekerforening Har du opdaget en fejl eller vil du rette data? + Bemærkning til apoteker: Vi indhenter kontaktoplysninger og information om apoteker fra mein-apothekenportal.de fra den tyske apotekerforening Har du opdaget en fejl eller vil du rette data? Lær mere - apoteker + Apoteker Desværre virkede det ikke \uD83D\uDE15 Prøv det igen. Indtast adgangskode Yderligere Tilgængelighed - zoom - Giver dig mulighed for at forstørre appen ved at knibe eller sprede fingrene (knib for at zoome). + Zoom + Giver dig mulighed for at forstørre appen ved at knibe for at zoome. adgangskode Sikre dine data med en adgangskode efter eget valg. adgangskode - Gem på computer + Gemme Vis adgangskode Gentag adgangskode Anbefalinger: %s - skrive mail - Når du sender din besked, vil følgende oplysninger om den anvendte hardware og det anvendte operativsystem blive transmitteret: + Skriv e-mail + Når du sender din besked, overføres følgende oplysninger om den anvendte hardware og operativsystem: Indløs kun på stedet Du kan endnu ikke sende e-recepter til dette apotek. I øjeblikket åben @@ -173,16 +173,16 @@ Forstået Gentagne password-matches Fejl 20 10 76631 - Dit sundhedskortbevis er ugyldigt. Er dit kort udløbet? Kontakt venligst din sygesikring. + Dit sundhedskortbevis er ugyldigt. Måske er dit kort udløbet? Kontakt venligst dit sygeforsikringsselskab. Mislykkede loginforsøg %s mislykkede loginforsøg blev fundet. %s mislykkede loginforsøg blev fundet. Vælg den bedste sikkerhedskopiering af enheden - Dette kan være et fingeraftryk, swipe-mønster eller lignende - tokens - adgangstoken + Dette kan være et fingeraftryk, swipe-mønster eller noget lignende + Poletter + Adgangstokens SSO-tokens Ingen adgangstoken tilgængelig intet tilgængeligt SSO-token @@ -193,26 +193,26 @@ ingen forbindelse til serveren Prøv venligst igen om et par minutter Indlæs igen - vise poletter + Vis tokens Hvordan vil du gerne sikre appen? Varsel Der er ikke konfigureret sikkerhedskopiering af enheden for denne enhed - Vi anbefaler, at du yderligere beskytter dine medicinske data med enhedssikkerhed såsom en adgangskode eller biometri. + Vi anbefaler, at du yderligere beskytter dine medicinske oplysninger med enhedssikkerhed såsom en kode eller biometri. Vis ikke denne meddelelse igen i fremtiden. Forbindelsen mislykkedes. En netværksforbindelse kunne ikke etableres. - Kommunikation med serveren mislykkedes: statuskode %s . - Kunne ikke kommunikere med serveren: Tjek venligst internetforbindelsen og indstillingerne for tid/dato. + Kommunikation med server mislykkedes: statuskode %s . + Kommunikation med server mislykkedes: Tjek venligst internetforbindelse og klokkeslæt/datoindstillinger. advarsel Din enhed kan have nedsat sikkerhed - Dette kan for eksempel være forårsaget af manipulerede enheder eller en aktiveret udviklertilstand. Af sikkerhedsmæssige årsager anbefaler vi ikke at bruge appen på jailbroken enheder. - Jeg anerkender den øgede risiko og ønsker stadig at fortsætte. + Dette kan for eksempel være forårsaget af manipulerede enheder, eller når udviklertilstand er slået til. Vi anbefaler ikke at bruge appen på jailbroken enheder af sikkerhedsmæssige årsager. + Jeg anerkender den øgede risiko og vil stadig gerne fortsætte. Hvorfor er enheder med root-adgang en potentiel sikkerhedsrisiko? Lær mere https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html - Profil navn + Navn på profilen Indtast venligst et navn til den nye profil. - profil navn - profiler + Profil navn + Profiler Sådan genkender du et NFC-aktiveret sundhedskort Ingen kontakt mulig via denne app Brug venligst de sædvanlige kanaler til at kontakte dit forsikringsselskab. @@ -220,12 +220,12 @@ Kun pinkode Tilmelding i e-recept-appen Navnefeltet må ikke være tomt. - Der findes allerede en profil med det indtastede navn. + Der findes allerede en profil med det navn, du har indtastet. profil %s valgt baggrundsfarve - forårsgrå - soldug + Forårsgrå + Soldug Det! Er! Lyserød! Træ Blå måne september @@ -233,16 +233,16 @@ Bundet sammen Sidst tilsluttet den %s Vil du slette profilen? - Dette vil slette alle profildata på denne enhed. Dine recepter i sundhedsnetværket forbliver intakte. - Sluk - Afbryde + Dette sletter alle data fra profilen på denne enhed. Dine recepter i sundhedsnetværket vil blive bibeholdt. + Slet + Afbestille slette profil Du vil slette den sidste profil. Appen kræver mindst én profil. Indtast venligst et navn til den nye profil. Fejl 20 10 76831 - Biblioteket med sundhedskort kunne ikke nås. Prøv igen. - Du kan finde ekspertverificerede oplysninger om sygdomme, ICD-koder og om forebyggelses- og plejespørgsmål på den nationale sundhedsportal. - Åbn Gesund.bund.de + Sundhedskortets bibliotek kunne ikke nås. Prøv igen. + Du kan finde ekspertverificeret information om sygdomme, ICD-koder og forebyggelses- og plejeemner i den nationale sundhedsportal. + Åbn health.bund.de Vi har ændret privatlivspolitikken E-recept-appen har udviklet sig. Dette har gjort det nødvendigt at opdatere vores privatlivspolitik. Åbn privatlivspolitik @@ -251,7 +251,7 @@ Hvad sker der, hvis jeg bruger kamerafunktionen/læser opskrifter med kameraet? Ingen nye opskrifter tilgængelige - %s ny opskrift + %s nye opskrift %s nye opskrifter Indløses @@ -261,80 +261,80 @@ Se adgangslogfiler Hvem fik adgang til dine opskrifter og hvornår? Adgangsnøgle til receptservicen - adgangslogfiler + Adgangslogfiler Ingen adgangslogs - Der er endnu ingen adgangslogfiler. + Der er ingen adgangslogfiler endnu. Opskriften er i gang og kan ikke slettes Acceptere Det virkede åbenbart ikke - Vi er klar over, at sammenhængen med sundhedskortet har sine faldgruber. Fremover bør registrering derfor også være mulig via en allerede godkendt sygesikringsapp. \n\n Vi arbejder også på at gøre det muligt at indløse recepter digitalt uden registrering. \n\n Har du bemærket noget under denne proces, som du gerne vil dele med os? Skriv venligst til os, vi er også glade for at modtage meget kritisk feedback. + Vi er klar over, at sammenhængen med sundhedskortet har sine faldgruber. Fremover skal registrering også være mulig via en allerede godkendt sygesikringsapp. \n\n Vi arbejder også på at sikre, at recepter kan indløses digitalt uden tilmelding. \n\n Har du bemærket noget under denne proces, som du gerne vil dele med os? Skriv venligst til os, vi vil også gerne modtage meget kritisk feedback. Tilslutningstips - Forbedre styrken af forbindelsen + Forbedre styrken af ​​forbindelsen Fjern om nødvendigt beskyttelsesdækslet. - Hvis enheden vibrerer og derefter afbryder forbindelsen, skal du søge efter den optimale position inden for en lille radius. + Hvis enheden vibrerer, og forbindelsen derefter afbrydes, skal du se efter den optimale position inden for en lille radius. Flyt enheden meget langsomt hen over kortet. Placer enheden direkte på kortet. For at gøre dette skal du placere sundhedskortet på en flad overflade (f.eks. et bord). - Forbedre styrken af forbindelsen - Bemærk placeringen af NFC-sensoren - Find ud af, hvor NFC-sensoren er placeret i din enhed (her f.eks. en oversigt for enheder fra %s ). - I nogle tilfælde kan positionen af NFC-sensoren variere inden for en modelserie (her f.eks. oplysningerne for %s ). + Forbedre styrken af ​​forbindelsen + Bemærk placeringen af ​​NFC-sensoren + Find ud af, hvor NFC-sensoren er placeret i din enhed (her f.eks. en oversigt over enheder fra %s ). + I nogle tilfælde kan positionen af ​​NFC-sensoren variere inden for en modelserie (her f.eks. informationen for %s ). Næste tip Yderligere Tæt - Prøv + Prøve skriv til os - Apotek søgelicens + Licens Apotekssøgning Indløs - Scannet recept + Scannet opskrift Scannet den %s Markeret som indløst den %s Hvordan vil du fortsætte? Bestille Tilgængelig snart - Reserver nu til afhentning eller få det leveret med kurerservice eller forsendelse + Reserver nu til afhentning eller få det leveret med kurer eller forsendelse Gem til senere bestilling Gem opskrifter på enheden Fortsæt med %s opskrift Fortsæt med %s opskrifter - Sundhedskortet kunne ikke tilsluttes - Den aktuelle profil er allerede forbundet med et andet sundhedskort (sygesikringsnummer %s ). - Dit sundhedskort er allerede forbundet med en anden profil. Skift til profil %s . - Gem på computer - kontaktoplysninger og adresse + Tilslutning til sundhedskort mislykkedes + Den aktuelle profil er allerede knyttet til et andet sundhedskort (sygesikringsnummer %s ). + Dit sundhedskort er allerede knyttet til en anden profil. Gå til profil %s . + Gemme + Kontaktoplysninger og adresse Kontakt - telefonnummer - Angiv venligst et telefonnummer for kontakt. - Mailadresse (valgfrit) + Telefon nummer + Angiv venligst et telefonnummer for at kontakte os. + E-mailadresse (valgfrit) Leveringsadresse - fornavn og efternavn - Indtast et for- og efternavn til kontaktformål. + Fornavn og efternavn + Angiv et for- og efternavn for at kontakte os. Gade og husnummer - Indtast venligst gade og husnummer, så vi kan kontaktes. + Angiv venligst et gade- og husnummer for at kontakte os. Yderligere adresse (valgfrit) - Leveringsinstruktion (valgfrit) + Leveringsvejledning (valgfrit) Yderligere kontaktoplysninger påkrævet Vil du kassere ændringer? kassere Til søgningen bruger apotekskataloget geo-koordinater, der blev bestemt ved hjælp af OpenStreetMap. Vi takker projektet for denne hjælp. - © OpenStreetMap ( %s ) + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Privatliv og brug + Databeskyttelse og brug Yderligere Du har modtaget din pinkode i et brev fra dit sygesikringsselskab. Ingen pinkode modtaget pinkode - Tjek forbindelsen til internettet og klokkeslættet/datoindstillingen på din enhed. + Tjek din enheds internetforbindelse og indstillinger for tid/dato. For at logge på, tryk på \"Lås op\". - låst ude? Bekræft venligst dine biometriske legitimationsoplysninger på denne enhed. + Låst ude? Bekræft venligst dine biometriske legitimationsoplysninger på denne enhed. Glemt kodeord? Slet venligst appen og geninstaller den. Du kan finde ud af hvorfor i vores %s . hjælpeområde - pakkestørrelse og enhed + Pakkestørrelse og enhed aktiv ingrediens - Mængden af aktiv ingrediens - batchbetegnelse + Mængden af ​​aktiv ingrediens + Batch navn Exp kategori Vaccine @@ -350,35 +350,35 @@ Adgangskoden er ikke synlig biometri adgangskode - venter på svar - Ingen recepter + Venter på svar + Ingen opskrifter Du har i øjeblikket ingen indløselige recepter. At opdatere Automatisk logout Af sikkerhedsmæssige årsager afbrydes forbindelsen til receptserveren efter 12 timer. Tilslut igen for at få aktuelle opskrifter. Forbinde - Har du modtaget en papirkopi? + Har du modtaget en papirudskrift? Tilføj opskrifter til din liste ved at trykke på scanningsknappen i øverste højre hjørne. Scan papirudskrift - Du skal være logget ind for at modtage opskrifter automatisk. + For at modtage recepter automatisk skal du være logget ind. Tilmeld Ingen indløste recepter Dine indløste recepter vises her. Af databeskyttelsesmæssige årsager vil dine opskrifter blive slettet fra opskriftsserveren efter 100 dage. Ingen indløste recepter - Dine indløste recepter vises her. Tilføj recepter via scanning for at begynde at indløse. - enhedshåndtering + Dine indløste recepter vises her. Tilføj opskrifter via scanning for at begynde at indløse. + Enhedshåndtering Tilsluttede enheder Registreret siden %s (denne enhed) Registreret siden %s - Af sikkerhedsmæssige årsager afbrydes forbindelsen til receptserveren efter 12 timer. For at oprette forbindelse igen skal du bruge dit sundhedskort og din pinkode for hver forbindelsesproces. + Af sikkerhedsmæssige årsager afbrydes forbindelsen til receptserveren efter 12 timer. For at genoprette forbindelsen skal du bruge et sundhedskort og en pinkode for hver forbindelsesproces. pinkode - Indtast din PIN-kode (sundhedskort). + Indtast PIN-kode (sundhedskort). Yderligere Tilmeld Tilsluttede enheder - fjerne enheden? - Afbryde - Fjernet + Vil du fjerne enheden? + Afbestille + Fjerne Vil du fjerne denne enhed? Vil du fjerne %s ? Hvis du fjerner %s , vil forbindelsen til opskriftsserveren blive afbrudt permanent om senest 12 timer. @@ -391,32 +391,32 @@ wwweg… Ingen internetforbindelse. Medicin og forbindinger - narkotika - Udlevering af receptpligtig medicin efter § 4 AMVV + Narkotika + Udlevering af receptpligtig medicin i henhold til § 4 AMVV Har du brug for hjælp? Vi har samlet nogle tips til dig for at løse de mest almindelige problemer. Startforbindelsestips - låse op - kortet er spærret + Lås op + kort spærret PIN-koden blev indtastet forkert tre gange. Dit kort er derfor blevet spærret af sikkerhedsmæssige årsager. - låse op kortet + Lås kortet op Indtast PUK Med din PIN-kode har du modtaget en 8-cifret PUK fra dit forsikringsselskab. Vælg ny pinkode Du kan selv vælge dit nye personlige identifikationsnummer (PIN) (6 til 8 cifre). - PIN-kode husket? - Noter venligst din PIN-kode og opbevar den et sikkert sted. - Afbryde + Har du husket din pinkode? + Skriv venligst din pinkode ned og opbevar den et sikkert sted. + Afbestille Okay - Det er ikke muligt at låse op - Du har nået det maksimale antal kortoplåsninger med denne PUK eller indtastet den forkert gentagne gange. Kontakt venligst dit forsikringsselskab. + Oplåsning ikke mulig + Du har nået det maksimale antal kortoplåsninger med denne PUK eller har gentagne gange indtastet den forkert. Kontakt venligst dit forsikringsselskab. Du kan bruge én PUK til op til 10 oplåsninger. - kort låst op + Kort oplåst Hvad du har brug for: - dit sundhedskort + Dit sundhedskort PUK på dit sundhedskort Yderligere - sundhedskort + Sundhedskort Bestil pinkode eller kort Tilmeld Hvordan vil du logge ind? @@ -424,87 +424,87 @@ PIN-kode til sundhedskortet Har du ikke et NFC-aktiveret sundhedskort og pinkode endnu? Ansøg nu - Eller: Log på med %s . + Eller: Log ind med %s . Din sygesikring app - "Dit adgangsnummer kan findes i øverste højre hjørne af dit sundhedskort." + "Du kan finde dit adgangsnummer i øverste højre hjørne af dit sundhedskort." Mit kort har ikke et adgangsnummer - Du har %s forsøg mere, før dit kort blokeres. + Du har %s et forsøg mere, før dit kort blokeres. Du har %s forsøg mere, før dit kort blokeres. - Sæt sundhedskortet på bagsiden af telefonen + Læg sundhedskortet på bagsiden af ​​telefonen Den følgende proces kan tage op til 30 sekunder. - Placer kortet %s på bagsiden af telefonen. - i øverste højre hjørne - i den øverste midte - øverst til venstre + Placer kortet %s på bagsiden af ​​telefonen. + i det øverste højre område + i midten i det øverste område + i det øverste venstre område i det midterste område til højre midten - i midten til venstre + i det midterste område til venstre i det nederste højre område - i den nederste midte - nederst til venstre + i midten i det nederste område + i det nederste venstre område Hjælp Sendt for %s minutter siden Sendt den %s Sendt lige nu - Sendt klokken %s + Sendt kl. %s Ikke længere gyldig - Log ind med appen - vælge forsikring + Tilmeld dig med app + Vælg forsikring Fandt du ikke det, du ledte efter? Denne liste bliver konstant udvidet. Registrering med et sundhedskort understøttes allerede af alle sygeforsikringsselskaber. Feedback fra e-recept-appen - Vi ser frem til din feedback. Brug venligst pladsen nedenfor og vær så præcis som muligt: + Vi ser frem til din feedback. Brug venligst følgende plads og vær så præcis som muligt: PUK Tæt - Hvor er det synd… - Din enhed opfylder desværre ikke minimumskravene for at logge ind på e-recept-appen. Mindst Android 7 og en NFC-chip er påkrævet for sikker autentificering med dit sundhedskort. + Hvor er det ærgerligt… + Din enhed opfylder desværre ikke minimumskravene for registrering i e-recept-appen. For sikker autentificering med dit sundhedskort kræves der mindst Android 7 og en NFC-chip. Lær mere Vil du gemme login-data? - Gem på computer + Gemme Gem ikke Varsel - Af sikkerhedsmæssige årsager afbrydes forbindelsen til receptserveren efter 12 timer. For at oprette forbindelse igen skal du bruge et sundhedskort og en pinkode for hver forbindelsesproces. + Af sikkerhedsmæssige årsager afbrydes forbindelsen til receptserveren efter 12 timer. For at genoprette forbindelsen skal du bruge et sundhedskort og en pinkode for hver forbindelsesproces. Opsæt biometrisk sikkerhed - Det er ikke muligt at gemme adgangsdata. Konfigurer biometrisk sikkerhed (f.eks. fingeraftryk) på din enhed på forhånd. - Afbryde - Ideer + Det er ikke muligt at gemme adgangsdata. Indstil på forhånd biometrisk sikkerhed (f.eks. fingeraftryk) på din enhed. + Afbestille + Indstillinger Varsel Acceptere Sikkerhed for dine receptdata - \"Denne app bruger den mest sikre biometriske sensor fra din enhed til at gemme dine legitimationsoplysninger i et sikkert område af enhedens hukommelse.\" - Den biometriske sikkerhed af dine adgangsdata giver dig mulighed for at åbne denne app i fremtiden uden at skulle indtaste din pinkode eller et sundhedskort, og for at se, ringe op, indløse eller slette recepter. + \"Denne app bruger den mest sikre biometriske sensor fra din enhed til at sikre dine legitimationsoplysninger i et beskyttet område af enhedens lager.\" + Den biometriske sikkerhed af dine adgangsdata giver dig mulighed for at åbne denne app i fremtiden, se, hente, indløse eller slette recepter uden et sundhedskort og indtaste din pinkode. Sørg for, at personer, som du må dele denne enhed med, og hvis biometriske karakteristika kan være gemt på denne enhed, også har adgang til dine recepter. det virkede desværre ikke - Godkendelse med sygesikringsappen mislykkedes. - Udløb %s + Godkendelse med sygesikringsappen lykkedes ikke. + Udløb den %s Opskriften er allerede blevet slettet fra serveren - Ret venligst dit input eller kasser ændringer + Ret venligst din indtastning eller kasser ændringer Korrekt - forsikrede data + Forsikrede persondata Efternavn Forsikring - forsikringsnummer - kortadgangsnummer + Forsikringsnummer + Kortadgangsnummer Tilmeld - Afmeld - Gem på computer + Log ud + Gemme Lave om Rediger profilbillede Yderligere serveren svarer ikke Prøv igen senere. Prøv igen - Se efter forsikring + Søg efter forsikring Vil du oprette forbindelse til opskriftsserveren nu? Logget ind forbindelse afbrudt Vil du oprette forbindelse til opskriftsserveren nu? Ingen tokens - Du modtager et token, når du er logget ind på receptservicen.\n + Du modtager et token, når du er logget ind på receptservicen.\n Ordre:% s Vælg den ønskede PIN-kode - låse op kortet + Lås kortet op Vælg PIN Gentag PIN-koden Indtastningerne adskiller sig fra hinanden. @@ -513,13 +513,13 @@ Lige nu Klokken %s Indkøbskurven er klar - Opskriften er tilføjet til din indkøbskurv. Gå til apotekets hjemmeside for at gennemføre bestillingen. + Opskriften er blevet tilføjet til din indkøbskurv. Gå til apotekets hjemmeside for at fuldføre ordren. Åben indkøbskurv Vis denne afhentningskode på apoteket. - Modtag afhentningskode + Afhentningskode modtaget Meddelelsen kan ikke vises Kontakt venligst dit apotek ( %s ). - Vis kurvlink + Vis indkøbskurvlink Vis afhentningskode Vis beskeden %s klokken %s @@ -527,53 +527,53 @@ Ordreoversigt Ny Rute - Bestille - Gratis for den, der ringer. Servicetider: Man-fre 8:00 - 20:00 undtagen på nationale helligdage + Rækkefølgen + Gratis for den, der ringer op. Tjenestetider: Man - Fre 8:00 - 20:00 undtagen på nationale helligdage Apotek Vælg den ønskede PIN-kode Ønsket pinkode gemt - I øjeblikket åben og i nærheden af mig + I øjeblikket åben og i nærheden af ​​mig Sorter efter … start søgning - direkte opgave - apoteker + Direkte opgave + Apoteker Telefonnummer (valgfrit) Søg på navn eller adresse Ingen gyldige apoteksoplysninger Der blev ikke fundet nogen aktuelle oplysninger om dette apotek. Indgangen til dette apotek vil blive slettet. Okay Apotekets bibliotek er ikke tilgængeligt - I øjeblikket kan der ikke hentes aktuelle oplysninger om dette apotek. Tjek venligst din internetforbindelse. - Afbryde + Der er i øjeblikket ingen aktuelle oplysninger om dette apotek. Tjek venligst din internetforbindelse. + Afbestille Prøv igen Gem miljø Login er ikke muligt - Det ser ud til, at dine biometriske loginoplysninger er ændret. Tilmeld dig igen med dit sundhedskort. - Afbryde + Det ser ud til, at dine biometriske login-egenskaber har ændret sig. Log venligst på igen med dit sundhedskort. + Afbestille Tilmeld - profil 1 + Profil 1 Tæt på mig Indløses senere Kan indløses fra %s - produktforbedringer - Anonym Analyse - Hjælp os med at gøre denne app bedre. Alle brugsdata indsamles anonymt og bruges kun til at forbedre brugeroplevelsen. - enhedssikkerhed + Produktforbedringer + Anonym analyse + Hjælp os med at gøre denne app bedre. Alle brugsdata indsamles anonymt og bruges udelukkende til at forbedre brugeroplevelsen. + Enhedssikkerhed personlige indstillinger Tilgængelighed - produktforbedringer + Produktforbedringer Tilføjet opskrift Opskriften er allerede tilgængelig Der opstod en fejl under import - Sluk - Scannet recept - Erstatning mulig + Slet + Scannet opskrift + Udskiftningsforberedelse mulig Glemt pinkode - %s opskrift + %s Opskrift %s Opskrifter - Jeg har læst og accepterer privatlivspolitikken og brugsbetingelserne. + Jeg har læst og accepterer privatlivspolitikken og vilkårene for brug. Data beskyttelse Vilkår for brug Vi vil gerne have: @@ -583,75 +583,74 @@ Du kan til enhver tid ændre denne beslutning i systemindstillingerne. Blive ved Acceptere - Denne app bruger den mest sikre metode fra din enhed. - Gem på computer + Denne app bruger den sikreste metode fra din enhed. + Gemme Vælge medicin - handelsnavn + Handelsnavn Ja Ingen dosering udstedelsesdato Denne recept vil blive indløst for dig som en del af en behandling. Ikke specificeret - ekstra betaling + Yderligere betaling medicin - Følgesedler + Indleveringsinstruktioner Berettiget ifølge BVG - alternativ forberedelse - opskriftens navn + Alternativ forberedelse + Opskriftens navn Emballage - håndarbejde instruktion + Fremstillingsvejledning Beskrivelse givet af udstedt den: aktiv ingrediens - ordineret + Foreskrevet Modtage Hvad er en direkte opgave? - Ved direkte henvisninger indløses en recept fra praksis eller hospital direkte på apoteket. Forsikrede behøver ikke at foretage sig noget og kan ikke gribe ind i indløsningsprocessen. \n\n Direkte henvisninger er angivet i e-recept-appen for at gøre din behandling mere gennemsigtig for dig. - nødhjælpsgebyr - Nogle gange er det nødvendigt at skynde sig. Nogle recepter kan indløses uden yderligere betaling af et nødhjælpsgebyr, såsom om natten eller på helligdage. - Narkotika er omfattet af egenbetaling - Fritaget for egenbetaling - Dem med lovpligtig sygesikring skal betale en egenbetaling på op til ti euro for receptpligtig medicin. \n\n Størrelsen på egenbetalingen afhænger af prisen på din medicin. Du skal selv betale for medicin, der koster mindre end 5 €.\n For medicin, der er dyrere, skal du betale ti procent af prisen, dog mindst 5 € og ikke mere end 10 €. \n\n Børn og unge under 18 år er generelt fritaget for egenbetaling. \n\n Hvis dine årlige udgifter til medicin overstiger din økonomiske grænse, kan du blive fritaget for egenbetalingen. Tal med dit sygeforsikringsselskab om dette. - Du er fritaget for egenbetaling af dette lægemiddel. Din sundhedsforsikring dækker udgifterne til lægemidlet. + Med direkte henvisning udfyldes en recept fra en praksis eller hospital direkte på et apotek. Forsikrede behøver ikke at foretage sig noget og kan ikke gribe ind i indløsningsprocessen. \n\n Direkte henvisninger er angivet i e-recept-appen for at gøre din behandling mere gennemsigtig for dig. + Akut servicegebyr + Nogle gange er hastværk nødvendigt. Nogle recepter kan udfyldes uden yderligere betaling af et akutgebyr, for eksempel om natten eller på helligdage. + Medicin mod egenbetaling + Fritaget for yderligere betaling + Dem med lovpligtig sygesikring skal betale en ekstra betaling på op til ti euro for receptpligtig medicin. \n\n Størrelsen af ​​den ekstra betaling afhænger af prisen på din medicin. Du skal selv betale for medicin, der koster mindre end 5 €.\n For medicin, der er dyrere, skal du betale ti procent af prisen, dog mindst 5 € og maksimalt 10 €. \n\n Børn og unge under 18 år er generelt fritaget for merbetaling. \n\n Hvis dine årlige udgifter til medicin overstiger din økonomiske byrdegrænse, kan du blive fritaget for egenbetalingen. Tal med dit sygeforsikringsselskab om dette. + Du er fritaget for at betale egenbetaling for denne medicin. Dit sygeforsikringsselskab vil dække udgifterne til medicinen. Hvor længe er denne recept gyldig? I denne periode kan du indløse din recept på ethvert apotek med en maksimal ekstra betaling på €10. - Erstatning mulig - På grund af dit sygeforsikringsselskabs lovkrav kan du få et alternativ med det samme aktive stof. \n\n Medicin kan se ud og hedde forskelligt, have forskellige priser og producenter, men stadig indeholde det samme aktive stof. Det aktive stof i sig selv og doseringen er særligt vigtige for lægemidlers virkning i kroppen. Patienter på apoteket får ofte et andet lægemiddel end det, lægen har ordineret på recepten – forudsat at lægemidlerne er sammenlignelige. Der kan være terapeutiske og økonomiske årsager til ændringen. - Scannet recept - Af sikkerhedsmæssige årsager må recepter importeret fra en papirudskrift ikke vise personlige eller medicinske data. \n\n Log ind på denne app med sundhedskort eller forsikringsapp for at se alle oplysninger i recepten. + Udskiftningsforberedelse mulig + På grund af lovkrav fra dit sygeforsikringsselskab kan du få et alternativ med det samme aktive stof. \n\n Medicin kan se ud og hedde anderledes, have forskellige priser og producenter, men stadig indeholde det samme aktive stof. Selve det aktive stof og doseringen er afgørende for lægemidlers virkning i kroppen. Patienterne får ofte en anden medicin på apoteket end den, lægen har ordineret – forudsat at medicinen er sammenlignelig. Der kan være terapeutiske og økonomiske årsager til ændringen. + Scannet opskrift + Recepter importeret fra en papirkopi kan ikke vise personlige eller medicinske oplysninger af sikkerhedsmæssige årsager. \n\n Log ind på denne app med sundhedskort eller forsikringsapp for at se alle oplysningerne i recepten. Opskriften er forkert Denne recept blev udstedt forkert. - Scannet recept - nødhjælpsgebyr + Akut servicegebyr Dosering i henhold til skriftlige instruktioner telefon - websted + internet side Post Det er ikke muligt at sortere efter afstand. Okay Indtast den aktuelle PIN-kode - Forkert PIN-kode indtastet + Forkert pinkode indtastet Den aktuelle pinkode på dit sundhedskort - kortet er spærret - Fjern blokeringen af dit kort i Indstillinger > Fjern blokering af kort. + kort spærret + Lås dit kort op i Indstillinger > Lås kort op. Af sikkerhedsmæssige årsager skal du indtaste din nuværende pinkode. Glemt pinkode Forkert opskrift medicin - Noget ser ud til at være gået galt under oprettelsen af din opskrift. Rapportere en fejl? + Noget ser ud til at være gået galt under oprettelsen af ​​din opskrift. Rapportere en fejl? Rapport Ikke logget ind Registreret med - sundhedskort + Sundhedskort biometri Ikke logget ind - Vi er interesserede i din mening. Brug venligst fem minutter på at besvare vores undersøgelse. Tak på forhånd. - advarselsmeddelelse + Vi er interesserede i din mening. Brug venligst fem minutter på at udfylde vores undersøgelse. På forhånd mange tak. + Advarselsmeddelelse Apotek føjet til favoritter - Fjernet apotek fra favoritter + Apotek fjernet fra favoritter Mine apoteker Adgangskodestyrke meget god Skrivehandling mislykkedes @@ -661,66 +660,66 @@ Adgangsreglen er overtrådt Du har ikke tilladelse til at få adgang til kortbiblioteket. Tildel din egen pin - Kortet er sikret med pinkode fra dit sygesikringsselskab (transport pinkode), angiv venligst din egen pinkode. + Kortet er sikret med PIN-kode fra dit sygesikringsselskab (transport-PIN) Indtast din egen PIN-kode. Adgangskode ikke fundet Der er ingen adgangskode gemt på dit kort. Du er blevet logget ud Log ind igen for at opdatere dine opskrifter. - aktiv ingrediens nummer + Aktiv ingrediens nummer styrke og enhed Indløst for %s minutter siden Indløst den %s Indløst lige nu Indløst klokken %s Ordre:% s - Denne recept blev indløst til dig som en del af en behandling. - nødhjælpsgebyr - Denne recept kan ikke udfyldes om natten på et apotek uden yderligere betaling af et akutgebyr. + Denne recept blev udfyldt som en del af en behandling for dig. + Akut servicegebyr + Denne recept kan ikke udfyldes på et apotek om natten uden yderligere betaling af et akutgebyr. Søg her - Ideer - Del placering i indstillinger. + Indstillinger + Del placering i Indstillinger. Tæt på mig - Hold nede for at redigere navnet. + Tryk og hold for at redigere navnet. Indtast det nye navn til profilen. - Du skal være logget ind for at modtage digitale recepter fra din praksis. - Modtage opskrifter digitalt? + For at modtage recepter digitalt fra din praksis skal du være logget ind. + Modtage recepter digitalt? Træk skærmen ned for at opdatere. - Ingen recepter + Ingen opskrifter Tilføj opskrifter ved hjælp af + knappen i øverste højre hjørne. Tilmeld - receptarkiv + Opskriftsarkiv Måske senere Tilmeld Rediger profilbillede - receptarkiv + Opskriftsarkiv Indtast navn - Gem på computer + Gemme Min bestilling Modtager: in - opskrifter + Opskrifter Apotek Sende Lave om Afhentes på apoteket Levering med kurer - Levering med post + Levering med postordre %s Opskrifter - Indløsning ikke muligt + Ikke muligt at indløse En eller flere recepter kunne ikke indløses. Ingen opskrift valgt For at indløse opskrifter skal der vælges mindst én opskrift. Tilføj kontaktoplysninger Lave om - Ingen recept + Ingen opskrift Du har i øjeblikket ingen indløselige recepter kollektion budbringer Forsendelse - vælge opskrifter + Vælg opskrifter Tryk her for at scanne opskrifter - Langt tryk for at redigere navne - Tilføj flere profiler, fx til dine børn eller forældre - Klik på displayet for at springe det viste værktøjstip over. + Tryk længe for at redigere navne + Tilføj yderligere profiler, for eksempel til dine børn eller forældre + Klik på displayet for at springe over det værktøjstip, der vises. Hvordan indløser man? Hvordan vil du gerne modtage din medicin? Indløs direkte @@ -728,10 +727,10 @@ Bestille Reserver eller få det leveret Parat - kollektiv kode - enkelte koder + Samlingskode + Individuelle koder - Du har %s recept. + Du har %s opskrift. Du har %s opskrifter. foretage et valg @@ -743,65 +742,65 @@ Varsel Denne app bruger software fra Google til at genkende koder. Lær mere - Om opskriftskodescanneren + Information om opskriftskodescanneren Hvilke data indeholder opskriftskoden? - Opskriftskoden indeholder kun en identifikator for opskriften. Det gør, at recepten kan findes på recepttjenesten i det digitale sundhedsnetværk. Receptkoden indeholder ingen data om dig eller din medicin. + Opskriftskoden indeholder kun en identifikator for opskriften. Det betyder, at recepten kan findes på receptservicen i det digitale sundhedsnetværk. Receptkoden indeholder ingen oplysninger om dig eller din medicin. Så ingen kan gøre noget med opskriftskoden alene? - Korrekt. Receptdataene skal downloades fra receptservicen. Dette kræver et sikkert login. + Korrekt. Receptdataene skal downloades fra receptservicen. Til dette kræves et sikkert login. Hvem kan tilmelde sig receptservicen? - Tilmelding til recepttjenesten i det digitale sundhedsnetværk er muligt for forsikrede, apoteker, lægepraksis og hospitaler. + Tilmelding til receptservicen i det digitale sundhedsnetværk er mulig for sikrede, apoteker, praksis og hospitaler. Hvorfor bruger e-recept-appen Google-funktioner? - Google tilbyder funktioner, der nemt kan indbygges i apps, og som konstant udvikles og opdateres af Google. Dette sikrer, at funktionerne fungerer på mange forskellige slutenheder og kan betjenes sikkert. Appen bruger en funktion til at forbedre kamera- og scanningsfunktionaliteten til Android-enheder (Google ML Kit). - Hvordan virker Google ML Kit-scanningsforbedring? + Google tilbyder funktioner, der nemt kan integreres i apps, og som Google løbende udvikler og opdaterer. Dette sikrer, at funktionerne fungerer på mange forskellige enheder og kan betjenes sikkert. Appen bruger en funktion til at forbedre kamera- og scanningsfunktionaliteten til Android-enheder (Google ML Kit). + Hvordan fungerer scanningsforbedring med Google ML Kit? Google ML Kit hjælper med at optimere billedet optaget af et kamera, så opskriftskoderne kan læses selv under dårlige lysforhold eller med ældre kameramodeller. - Vil data om recepten eller min medicin blive videregivet til Google? - Ingen. Den læste opskriftskode gemmes direkte i appen. Det vil ikke blive videregivet til Google. Receptdataene gemmes ikke i koden, kun i det digitale sundhedsnetværk. Derfra sendes de til appen. Google har ikke adgang til det digitale sundhedsnetværk. + Vil data om recepten eller min medicin blive delt med Google? + Ingen. Den læste opskriftskode gemmes direkte i appen. Det vil ikke blive delt med Google. Receptdataene gemmes ikke i koden, men kun i det digitale sundhedsnetværk. Derfra overføres de til appen. Google har ikke adgang til det digitale sundhedsnetværk. Hvilke data behandler Google, når du bruger ML Kit? - Google har kun adgang til tekniske oplysninger om den anvendte slutenhed og den generelle brug af tillægsfunktionen (f.eks. fejlrate, kameraindstillinger) for at kunne registrere dette statistisk og dermed forbedre tillægsfunktionen. Når du tilgår, registrerer Google midlertidigt IP-adressen på din slutenhed. Oplysninger om dig og indholdet af opskriften vil ikke blive registreret af Google. - Er brugen af Google ML Kit frivillig? - Ja. ML Kit er dog indbygget i opskriftskodescanneren i Android-versionen af e-recept-appen. Hvis du bruger opskriftskodescanneren på en Android-enhed, bruges ML Kit-funktionen også altid. Du kan dog undvære at bruge opskriftskodescanneren. Dine recepter kan også indlæses i appen, hvis du tilmelder dig det digitale sundhedsnetværk med det elektroniske sundhedskort eller via din sygesikringsapp. + Google får kun adgang til tekniske informationer om den anvendte enhed og den generelle brug af tillægsfunktionen (f.eks. fejlrate, kameraindstillinger) for at kunne registrere dette statistisk og dermed forbedre tillægsfunktionen. Når du får adgang, registrerer Google midlertidigt din enheds IP-adresse. Oplysninger om dig og indholdet af opskriften registreres ikke af Google. + Er brugen af ​​Google ML Kit frivillig? + Ja. ML Kit er dog indbygget i opskriftskodescanneren i Android-versionen af ​​e-recept-appen. Hvis du bruger opskriftskodescanneren på en Android-enhed, bruges ML Kit-funktionen altid. Du kan dog undgå at bruge opskriftskodescanneren. Dine recepter kan også indlæses i app\'en, hvis du logger på det digitale sundhedsnetværk med det elektroniske sundhedskort eller via din sygesikringsapp. Kan jeg se, hvem der har set mine opskrifter? Ja. Al adgang til dine data er fuldt logget i det digitale sundhedsnetværk. I e-recept-appen kan du se, hvem der har tilgået dine data. - Hvem kan jeg kontakte, hvis jeg har spørgsmål til appen eller e-recepten? - Du kan finde detaljerede oplysninger i databeskyttelseserklæringen. + Hvor kan jeg kontakte, hvis jeg har spørgsmål om appen eller e-recepten? + Detaljerede oplysninger kan findes i databeskyttelseserklæringen. Antal foreskrevne pakninger - Ingen recepter + Ingen opskrifter Til dette har du brug for indløselige recepter. - vælge forsikring - Se efter forsikring - Afbryde + Vælg forsikring + Søg efter forsikring + Afbestille Hvad vil du gerne ansøge om? Til denne app skal du bruge et kort og den tilhørende pinkode. Hvordan vil du kontakte dit forsikringsselskab? Dit forsikringsselskab tilbyder følgende kontaktmuligheder - Dit forsikringsselskab tilbyder følgende kontaktmuligheder + Dit forsikringsselskab tilbyder følgende kontaktmulighed Tæt PIN-koden er indtastet forkert. - Adgangsnummeret er indtastet forkert + Adgangsnummer indtastet forkert PUK indtastet forkert. udgiftskvitteringer - Vis udgiftskvitteringer + Se omkostningskvitteringer udgiftskvitteringer For at modtage udgiftskvitteringer skal du være tilsluttet serveren. Forbinde - Ingen kvitteringer + Ingen omkostningskvitteringer Deaktiver - Afbryde - deaktiver funktionen - Dette vil slette alle kvitteringer fra denne enhed og fra serveren. - Modtag udgiftskvitteringer + Afbestille + Deaktiver funktionen + Dette vil slette alle udgiftskvitteringer fra denne enhed og serveren. + Modtag omkostningskvitteringer Dine omkostningskvitteringer gemmes også på opskriftsserveren. - Modtage + Modtaget I alt: %s %s Vælge Dele - Sluk - Sluk + Slet + Slet Indsend %s € total pris - Tip: Indsend udgiftskvitteringer via forsikringsappen - Indsend omkostningskvitteringer nemt via dit forsikringsselskabs app. I det næste trin skal du vælge denne app og trykke på Del. + Tip: Indsend omkostningskvitteringer via forsikringsappen + Indsend omkostningskvitteringer nemt via dit forsikringsselskabs app. I næste trin skal du vælge denne app og trykke på del. Øve sig Apotek Dato @@ -809,44 +808,79 @@ Lægemiddel-id Udstedt for KVNR: %s - Fødselsdato: %s + Født den: %s Okay - Hvordan indsender du kvitteringer? - Overfør direkte til dit forsikringsselskabs/bistandskontors app. For at gøre dette skal du vælge appen på næste side. + Hvordan indsender du støttedokumenter? + Overfør direkte til appen på dit forsikrings-/ydelseskontor. For at gøre dette skal du vælge appen på næste side. eller - Gem filen og importer den senere til forsikrings-/hjælpemiddelportalen. + Gem filen og importer den senere til forsikrings-/ydelsesportalen. Artikel: %s - Nummer: %s + Antal: %s moms: %s %% Bruttopris i EUR: %s Yderligere gebyrer - nødhjælpsgebyr + Akut servicegebyr BTM gebyr - T receptgebyr - indkøbsomkostninger - Messenger service + T-receptgebyr + Anskaffelsesomkostninger + Kurertjeneste I alt i EUR: %s afgift Virkelig slette? - Filen slettes fra din enhed og fra serveren. - Sluk + Filen slettes fra din enhed og serveren. + Slet Udsendt Postnummer Beliggenhed - Indtast venligst dit postnummer for at kontakte os. - Indtast venligst din bopæl, når du kontakter os. + Angiv venligst dit postnummer for at kontakte os. + Angiv venligst din bopæl for at kontakte os. Vil blive indløst for dig Er blevet indløst for dig Du skal være logget ind for at bruge denne tjeneste. - forsikring app - sundhedskort + Forsikring app + Sundhedskort Tilknyttet pinkode påkrævet + Kan kun indløses i morgen som selvbetaler + Kun %s dage tilbage til at indløse som selvbetaler + \nKan stadig indløses som selvbetaler i %s dage\n + Kun gyldig i %s dage + \nGyldig i %s dage tilbage\n + Gælder kun i morgen + Der opkræves gebyrer + tager forsikring + Opskriften(erne) er blevet overført. + Opskriften kan ikke behandles. Prøv igen. Du skal muligvis vælge et andet apotek. + Opskriften kan ikke behandles. Apoteket melder om en ukendt fejl. Prøv eventuelt et andet apotek. + Recepten blev afvist af apoteket. Recepten kan være ugyldig, eller din leveringsadresse eller kontaktoplysninger kan være ugyldige. + Kan ikke indløses. Tjek venligst din internetforbindelse. + Opskriften blev overført. Apoteket melder dog om en behandlingsfejl. Kontakt venligst apoteket. + Recepten blev afvist af apoteket. Recepten er allerede indløst. + Recepten blev afvist af apoteket. Opskriften er blevet slettet. + Opskriften kunne ikke overføres. Tjek venligst din internetforbindelse, og prøv igen. + En eller flere opskrifter kunne ikke overføres. + Fejl ved afsendelse + Sendt med succes! + Fejl på apoteket + Fejl på apoteket + Kontakt apoteket + Recept allerede indløst + Opskriften er slettet + Intet internet For at modtage adgangslogfiler skal du være forbundet til serveren. - Du kan stadig udfylde recepten på et apotek inden for denne periode, men du skal selv betale hele købesummen for medicinen. Alternativt kan du bede din praksis om at få genudstedt recepten. + Du kan stadig udfylde recepten på apoteket inden for denne periode, men du skal selv betale hele købesummen for medicinen. Alternativt kan du bede din praksis om at få genudstedt recepten. Parat Anmod om rettelse På apoteket I appen Få denne kode scannet på dit apotek. Anmodning om faktureringskorrektion + medicin + Indtast venligst mindst 1 tegn. + Eller. Prøv appen i demotilstand + Demo-tilstand + Demo-tilstand + Brug demotilstand + Demotilstand aktiveret + Slut her + Aktiver demotilstand diff --git a/android/src/main/res/values-en/strings.xml b/app/features/src/main/res/values-en/strings.xml similarity index 77% rename from android/src/main/res/values-en/strings.xml rename to app/features/src/main/res/values-en/strings.xml index 3409a087..625e4eb0 100644 --- a/android/src/main/res/values-en/strings.xml +++ b/app/features/src/main/res/values-en/strings.xml @@ -4,7 +4,7 @@ Cancel Back at - Digital. Fast. Secure. + Digital. Fast. Safe. Task ID Access code Terms of Use @@ -16,14 +16,14 @@ This is not a valid prescription code This prescription code has already been scanned - %s prescription recognised - %s prescriptions recognised + %s recipe recognized + %s recipes recognized Cancel Camera light - Cancel scanning of prescription codes? - Cancel scanning - Continue + Cancel scanning? + OK + Don\'t cancel Let\'s go What you need: Enter card access number @@ -31,8 +31,8 @@ Try again Failed to connect to the server. - You have %s attempt remaining before your card is locked. - You have %s attempts remaining before your card is locked. + You have %s one more attempt before your card is blocked. + You have %s more attempts before your card is blocked. You will find the access number in the top right-hand corner of your medical card. Cancel @@ -41,7 +41,7 @@ Still searching ... Slowly move the card on the back of the device. Tip - Device covers may make it difficult to connect via NFC. + Device cases may make it difficult to connect via NFC. Card recognised Try not to move the medical card. Medical card found. Please do not move. @@ -55,7 +55,7 @@ Imprint Publisher gematik GmbH\nFriedrichstr. 136\n10117 Berlin, Germany - Managing Director: Dr. med. Markus Leyck Dieken\nRegister Court: District Court of Berlin-Charlottenburg\nCommercial register no.: HRB 96351\nVAT ID: DE241843684 + Managing Director: Dr. med. Markus Leyck Dieken\nRegister Court: Amtsgericht Berlin-Charlottenburg\nCommercial register no.: HRB 96351\nVAT ID: DE241843684 Responsible for the content Dr. med. Markus Leyck Dieken Contact @@ -67,7 +67,7 @@ Welcome Start login Unlock - Log in + Register Cancel Security Legal information @@ -78,14 +78,14 @@ Mark as redeemed Mark as not redeemed Dosage form - Standard size + Package size Insured person Name Address Date of birth Health insurance / cost unit Status - Insurance number + Policyholder number Prescriber Name Specialist physician @@ -95,7 +95,7 @@ Address Establishment number Telephone number - Email + Email address Accident at work Date of accident Accident company or employer number @@ -105,7 +105,7 @@ Opening hours Website Can only be redeemed today as self-paying customer - Log in + Register Enable NFC Please enable the NFC function on your device to log in with your medical card. Enable @@ -121,10 +121,10 @@ Settings Suppress screenshots Prevents the display of a preview image when switching apps - Do you consent to the anonymous analysis of usage behaviour by e-prescription? + Do you allow E-Prescription to analyze your usage behavior anonymously? Technical information Security of your prescription data - Please be aware that people with whom you may share this device and whose biometrics may be stored on this device or who have the device PIN, swipe pattern or password may also have access to your prescriptions. + Please be aware that people with whom you may share this device and whose biometrics may be stored on this device may also have access to your prescriptions. Sending failed No email program set up No results @@ -137,17 +137,17 @@ I would like to help improve the app This comprises the hardware and software information of your phone, e-prescription app settings and the extent of use, but never your personal or health data. The data is provided exclusively by data processing providers to gematik GmbH and deleted after a maximum of 180 days. You can disable the analysis again at any time via the menu in the app. - We can use this data to understand which functions are used frequently and improve them. Furthermore, we can assess how long older technology needs to be supported and when we can, for example, make a newer operating system version mandatory without affecting (too many) users. + This data allows us to understand which functions are frequently used and improve them. We can also estimate how long older technology needs to be supported and when we can, for example, make a newer operating system version mandatory without affecting (too many) users. Improve app Anonymous analysis remains disabled %s Thank you for your support! - Log in + Register Please identify yourself in order to download prescriptions. Note to pharmacies: we obtain the contact details for and information about pharmacies from mein-apothekenportal.de provided by the Deutscher Apothekenverband e.V. Have you found an error or would you like to correct any data? Find out more - Pharmacies + pharmacies Unfortunately that didn\'t work \uD83D\uDE15 - Please try again. + Please try it again. Enter password Next Accessibility aids @@ -164,20 +164,20 @@ The following information about the hardware and operating system you use is transferred when you send an email: Redeem on site only You cannot yet send e-prescriptions to this pharmacy. - Currently open + Open now courier service Mail order - Filter + Filters Filter No location available Agreed Repeated password matches Error 20 10 76631 - Your medical card\'s certificate is invalid. Your card may have expired. Please contact your health insurance company. + Your health card certificate is invalid. Maybe your card has expired? Please contact your health insurance company. Unsuccessful login attempts - %s unsuccessful login attempt detected. - %s unsuccessful login attempts detected. + %s unsuccessful login attempts were detected. + %s unsuccessful login attempts were detected. Select optimum device security This may be a fingerprint, swipe pattern or similar @@ -193,10 +193,10 @@ No connection to the server Please try again in a few minutes. Reload - Show tokens + Display tokens How would you like to secure this app? Note - No device backup has been set up for this device + No device security has been set up for this device We recommend that you add additional protection for your medical data by securing your device for instance with a code or biometrics. Do not show this message in future. Connection failed. A network connection could not be created. @@ -204,7 +204,7 @@ Failed to communicate with the server: Please check the internet connection and the time/date settings. Warning Your device may have reduced security - This can be caused, for example, by manipulated devices or an activated developer mode. For security reasons, we do not recommend using the app on jailbroken devices. + This can be caused, for example, by manipulated devices or when developer mode is switched on. We recommend not using the app on jailbroken devices for security reasons. I acknowledge the increased risk and would like to continue anyway. Why are devices with root access a potential security risk? Find out more @@ -233,7 +233,7 @@ Connected Last connected on %s Delete profile? - All data belonging to the profile will be deleted on this device. Your prescriptions in the health network will be retained. + This will delete all data from the profile on this device. Your prescriptions in the health network will be retained. Delete Cancel Delete profile @@ -241,7 +241,7 @@ The app requires at least one profile. Please enter a name for the new profile. Error 20 10 76831 The register of medical cards could not be reached. Please try again. - You can find professionally verified information on illnesses, ICD codes and issues around prevention and healthcare in the National Health Portal. + You can find professionally verified information on illnesses, ICD codes and issues to do with prevention and healthcare in the National Health Portal. Open gesund.bund.de We have amended the Privacy policy The e-prescription app has evolved, so we have had to update our Privacy policy. @@ -251,23 +251,23 @@ What happens if I use the camera function/read prescriptions using the camera? No new prescriptions available - %s new prescription - %s new prescriptions + %s new recipe + %s new recipes - Can be redeemed + Redeemable In redemption Redeemed Unknown - Show access logs - Here you can see who has accessed your prescriptions - This relates to access keys for the prescription service + Display access logs + Who accessed your prescriptions and when? + Access key to the prescription service Access logs No access logs No access logs are available yet. The prescription is currently being processed and cannot be deleted Accept That didn\'t seem to work - We are aware that connecting using your medical card has its quirks. For that reason, in future it should be possible to log in using a health insurance company\'s app that has previously been authenticated.\n\nWe are also working on enabling prescriptions to be redeemed digitally without the need to log in.\n\nDid you notice anything during this process that you would like to share with us? We look forward to even very critical feedback. + We are aware that the connection with the health card has its pitfalls. In the future, registration should also be possible via an already authenticated health insurance app. \n\n We are also working on ensuring that prescriptions can be redeemed digitally without registering. \n\n Did you notice anything during this process that you would like to share with us? Please write to us, we would also be happy to receive very critical feedback. Connection tips Increase the strength of the connection Remove the protective case if necessary. @@ -296,8 +296,8 @@ Save to order later on Save prescriptions on device - Continue with %s prescription - Continue with %s prescriptions + Continue with %s recipe + Continue with %s recipes Failed to connect medical card The current profile is already connected to a different medical card (health insurance number %s). @@ -324,7 +324,7 @@ Privacy & Use Next You received your PIN in a letter from your health insurance company. - PIN not received + No PIN received PIN Check your connection to the Internet and your device\'s time/date setting. To log in, press “Unlock”. @@ -342,7 +342,7 @@ Undo Note Help us make this app better - Select own password + Enter password The password needs to be at least eight characters long Password strength not sufficient Password strength sufficient @@ -361,7 +361,7 @@ Add prescriptions to your list by tapping the scan button in the top right corner. Scan paper print-out You need to be logged in to receive prescriptions automatically. - Log in + Register No redeemed prescriptions Your redeemed prescriptions are displayed here. Your prescriptions will be deleted from the prescription server after 100 days on data protection grounds. No redeemed prescriptions @@ -374,7 +374,7 @@ PIN Enter your PIN (medical card). Next - log in + Register Connected devices Remove device? Cancel @@ -397,39 +397,39 @@ We have put a few tips together for you to solve the most common problems. Launch connection tips Unlock - Card blocked + card blocked The PIN was entered incorrectly three times. Your card has therefore been blocked for security reasons. - Unlock card + unlock card Enter PUK - You will have received an 8-digit PUK along with your PIN from your health insurance company. + With your PIN you have received an 8-digit PUK from your insurance company. Select new PIN - You can choose your new personal identification number (PIN) with 6 to 8 digits. + You can choose your new personal identification number (PIN) yourself (6 to 8 digits). PIN remembered? Please make a note of your PIN and keep it in a safe place. Cancel OK Cannot unlock You\'ve used this PUK to unlock your card the maximum number of times or have repeatedly entered it incorrectly. Please contact your health insurance company. - You can use a PUK to unlock up to 10 times. - Card unlocked + You can use one PUK for up to 10 unlocks. + card unlocked What you need: Your medical card Medical card PUK Next - Medical card + Insurance card Order PIN or card - Log in + Register How do you want to sign in? NFC-enabled medical card Medical card PIN Don\'t have an NFC-enabled medical card and PIN yet? - Order now + order now Or: Sign in with your %s. health insurance company app "Your card access number is located in the top right-hand corner on the front of your medical card." - My medical card has no access number + My card does not have an access number - You have %s more attempt before your card is blocked. + You have %s one more attempt before your card is blocked. You have %s more attempts before your card is blocked. Place medical card on the back of the phone @@ -474,19 +474,19 @@ Security of your prescription data \"This app uses the most secure biometric sensor provided by your device to store your access data in the secure area of the device memory. \" Using biometric protection for your access data means that you can launch this app in future without your medical card and PIN in order to view, retrieve, redeem or delete prescriptions. - Please be aware that people with whom you may share this device and whose biometrics may be stored on this device or who have the device PIN, swipe pattern or password may also have access to your prescriptions. + Please be aware that people with whom you may share this device and whose biometrics may be stored on this device may also have access to your prescriptions. Unfortunately that didn\'t work Authentication with the health insurance company\'s app was not successful. Expired on %s The prescription has already been deleted from the server - Please correct your input or discard changes + Please correct your entry or discard changes Correct Policyholder details Name Insurance - Insurance number + Policyholder number Card access number - Log in + Register Log out Save Change @@ -495,17 +495,17 @@ Server not responding Please try again later. Try again - Look for insurance + Search for insurance Connect to the prescription server now? Logged in successfully connection lost Connect to the prescription server now? No tokens - You will receive a token when you are logged in to the prescription service.\n + You will receive a token when you are logged in to the prescription service.\n Orders Select desired PIN - Unlock card - Choose PIN + unlock card + Select PIN Repeat PIN The entries differ from each other. No orders @@ -515,11 +515,11 @@ Shopping cart is ready The prescription has been added to your shopping cart. Please go to the pharmacy\'s website to complete the order. Open shopping cart - Show this pick-up code at the pharmacy. + Show this collection code at the pharmacy. Receive pickup code Message cannot be displayed Please contact your pharmacy ( %s ). - Show cart link + Show shopping cart link Show pickup code Show the message %s at %s o\'clock @@ -527,51 +527,51 @@ Order overview New Course - an order - Free for the caller. Service times: Mon - Fri 8:00 a.m. - 8:00 p.m. except on national holidays + Order + Free of charge for the caller. Service times: Mon - Fri 8:00 a.m. - 8:00 p.m. except on national holidays Pharmacy Select desired PIN Desired PIN saved Currently open and near me Filter by … start search - direct assignment + Direct assignment pharmacies - phone number (optional) + Telephone number (optional) Search for name or address No valid pharmacy information No current information was found about this pharmacy. The entry for this pharmacy will be deleted. OK Pharmacy directory not available - Currently no current information about this pharmacy can be retrieved. Please check your internet connection. + Currently no current information about this pharmacy can be accessed. Please check your internet connection. Cancel Try again Save Environment - Sign-in not possible - It appears that your biometric login credentials have changed. Please register again with your health card. + Login is not possible + It appears your biometric login characteristics have changed. Please log in again with your health card. Cancel - Log in - profile 1 + Register + Profile 1 Close to me Redeemable later Redeemable from %s - product improvements - Anonymous Analysis - Help us make this app better. All user data is collected anonymously and is only used to improve the user experience. + Product improvements + Anonymous analysis + Help us make this app better. All usage data is collected anonymously and is used exclusively to improve the user experience. Device security personal settings Accessibility aids - product improvements + Product improvements Added prescription Prescription already imported An error occurred while importing Delete Scanned prescription Substitute medication possible - Forgot PIN + Forgotten PIN - %s Prescription - %s Prescriptions + %s Recipe + %s Recipes I have read and accept the privacy policy and terms of use. Privacy policy @@ -583,90 +583,89 @@ You can modify this decision in the system settings at any time Continue Accept - This app uses the most secure method provided by your device. + This app uses the safest method provided by your device. Save Choose Medicine - trade name + Trade name Yes - no + No dosage date of issue This prescription will be redeemed for you as part of a treatment. Not specified - additional payment + Additional payment Medicine - Delivery Notes + Submission instructions Eligible according to BVG - alternative preparation + Alternative preparation formula name Packaging - crafting instruction - description + Manufacturing instructions + Description given by issued on: Active substance - prescribed + Prescribed Receive What is a direct assignment? - In the case of direct referrals, a prescription from a practice or hospital is redeemed directly at a pharmacy. Insured persons do not have to take any action and cannot intervene in the redemption process. \n\n Direct referrals are listed in the e-prescription app to make your treatment more transparent for you. + With direct referral, a prescription from a practice or hospital is filled directly at a pharmacy. Insured persons do not have to take any action and cannot intervene in the redemption process. \n\n Direct referrals are listed in the e-prescription app to make your treatment more transparent for you. Emergency service fee Sometimes hurry is required. Some prescriptions can be redeemed without the additional payment of an emergency service fee, such as at night or on public holidays. - Drugs subject to co-payment - Exempted from co-payment - Those with statutory health insurance must pay a co-payment of up to ten euros for prescription drugs. \n\n The amount of the co-payment depends on the price of your medication. You have to pay for medicines that cost less than €5 yourself.\n For medicines that are more expensive, you have to pay ten percent of the price, but at least €5 and a maximum of €10. \n\n Children and young people under the age of 18 are generally exempt from co-payment. \n\n If your annual costs for medication exceed your financial limit, you can be exempted from the co-payment. Talk to your health insurer about this. - You are exempt from the co-payment of this drug. Your health insurance will cover the cost of the medication. - How long is this prescription valid? + Medications subject to co-payment + Exempt from additional payment + Those with statutory health insurance must pay an additional payment of up to ten euros for prescription medication. \n\n The amount of the additional payment depends on the price of your medication. You have to pay for medications that cost less than €5 yourself.\n For medicines that are more expensive, you have to pay ten percent of the price, but at least €5 and a maximum of €10. \n\n Children and young people under the age of 18 are generally exempt from additional payment. \n\n If your annual costs for medication exceed your financial burden limit, you can be exempt from the co-payment. Talk to your health insurance company about this. + You are exempt from paying a co-payment for this medication. Your health insurance company will cover the cost of the medication. + How long is this prescription valid for? During this period, you can redeem your prescription in any pharmacy with a maximum additional payment of €10. Substitute medication possible - Due to the legal requirements of your health insurance company, you can be given an alternative with the same active ingredient. \n\n Medicines can look and be called differently, have different prices and manufacturers, but still contain the same active ingredient. The active ingredient itself and the dosage are particularly important for the effect of drugs in the body. Patients in the pharmacy often get a different drug than the one prescribed by the doctor on the prescription - provided the drugs are comparable. There can be therapeutic and economic reasons for the change. + Due to legal requirements from your health insurance company, you may be given an alternative with the same active ingredient. \n\n Medicines can look and be called different, have different prices and manufacturers, but still contain the same active ingredient. The active ingredient itself and the dosage are crucial for the effect of medicines in the body. Patients often receive a different medication at the pharmacy than the one prescribed by the doctor - provided the medication is comparable. There may be therapeutic and economic reasons for the change. Scanned prescription - For security reasons, prescriptions imported from a paper printout must not display any personal or medical data. \n\n Sign in to this app with health card or insurance app to view all information contained in the prescription. + Prescriptions imported from a hardcopy cannot display personal or medical information for security reasons. \n\n Log in to this app with health card or insurance app to view all the information contained in the prescription. Prescription incorrect This prescription was issued incorrectly. - Scanned prescription - emergency service fee + Emergency service fee Dosage according to written instructions Phone - site + website Email Sorting by distance not possible. OK Enter current PIN - Incorrect PIN entered + Wrong PIN entered The current PIN of your health card - Card blocked + card blocked Unblock your card in Settings > Unblock card. For security reasons, please enter your current PIN. - Forgot PIN + Forgotten PIN Defective prescription Medicine Something seems to have gone wrong when creating your prescription. Report an error? Report Not logged in Registered with - Medical card + Insurance card Biometrics Not logged in - We are interested in your opinion. Please take five minutes to answer our survey. Thank you in advance. - warning notice + We are interested in your opinion. Please take five minutes to complete our survey. Thank you very much in advance. + Warning notice Pharmacy added to favorites Removed Pharmacy from Favorites My pharmacies Password strength very good - Write operation unsuccessful + Write operation not successful PIN could not be saved Report Assign PIN Access rule violated You do not have permission to access the map directory. Assign your own pin - The card is secured with a PIN from your health insurance company (transport PIN), please assign your own PIN. + The card is secured with a PIN from your health insurance company (transport PIN). Please enter your own PIN. Password not found There is no password stored on your card. You have been logged out - Sign in again to update your prescriptions. - active ingredient number + Sign in again to update your recipes. + Active ingredient number potency and unity Redeemed %s minutes ago Redeemed on %s @@ -674,23 +673,23 @@ Redeemed at %s o\'clock Orders This prescription was redeemed for you as part of a treatment. - emergency service fee - This prescription cannot be filled at night in a pharmacy without the additional payment of an emergency service fee. + Emergency service fee + This prescription cannot be filled at a pharmacy at night without additional payment of an emergency service fee. Search here Settings - Share location in settings. + Share location in Settings. Close to me - Hold to edit the name. + Press and hold to edit the name. Enter the new name for the profile. - You must be logged in to receive digital prescriptions from your practice. + To receive prescriptions digitally from your practice, you must be logged in. Receive prescriptions digitally? - Drag the screen down to refresh. + Pull down the screen to refresh. No prescriptions Add prescriptions using the + button in the top right corner. - log in + Register prescription archive Vielleicht später - log in + Register Edit profile picture prescription archive Enter name @@ -700,27 +699,27 @@ Prescriptions Pharmacy Send - To change + Change Pick up at the pharmacy Delivery by courier - Delivery by mail + Delivery by mail order %s Prescriptions - Redeem not possible + Not possible to redeem One or more prescriptions could not be redeemed. No prescription selected To redeem prescriptions, at least one prescription must be selected. - Add contact information - To change - No prescription + Add contact details + Change + No recipe You currently have no redeemable prescriptions - pickup + collection courier Mail order choose prescriptions Tap here to scan prescriptions Long press to edit names Add more profiles, eg for your children or parents - Click on the display to skip the displayed tool tip. + Click on the display to skip the tooltip that appears. How to redeem? How would you like to receive your medication? Redeem directly @@ -728,11 +727,11 @@ Order Reserve or have it delivered Done - collective code - single codes + Collection code + Individual codes - You have %s prescription. - You have %s prescriptions. + You have %s recipe. + You have %s recipes. Make a selection All prescriptions @@ -747,51 +746,51 @@ What data does the prescription code contain? The prescription code contains only an identifier of the prescription. This allows the prescription to be found on the prescription service in the digital health network. The prescription code does not contain any data about you or your medication. So nobody can do anything with the prescription code alone? - Correct. The prescription data must be downloaded from the prescription service. This requires a secure login. + Correct. The prescription data must be downloaded from the prescription service. A secure login is required for this. Who can register for the prescription service? - Registering with the prescription service in the digital health network is possible for insured persons, pharmacies, medical practices and hospitals. + Registration for the prescription service in the digital health network is possible for insured persons, pharmacies, practices and hospitals. Why does the e-prescription app use Google features? - Google offers functions that can be easily built into apps and that are constantly being developed and updated by Google. This ensures that the functions work on many different end devices and can be operated securely. The app uses a feature to improve camera and scanning functionality for Android devices (Google ML Kit). - How does Google ML Kit scan enhancement work? + Google offers functions that can be easily integrated into apps and that Google continually develops and updates. This ensures that the functions work on many different devices and can be operated safely. The app uses a feature to improve the camera and scanning functionality for Android devices (Google ML Kit). + How does scanning enhancement work with Google ML Kit? Google ML Kit helps to optimize the image captured by a camera so that the prescription codes can be read even in poor lighting conditions or with older camera models. - Will data about the prescription or my medication be passed on to Google? - No. The read prescription code is saved directly in the app. It will not be passed on to Google. The prescription data is not stored in the code, only in the digital health network. From there they are sent to the app. Google does not have access to the digital health network. + Will data about the prescription or my medication be shared with Google? + no The read prescription code is saved directly in the app. It will not be passed on to Google. The prescription data is not stored in the code, only in the digital health network. From there they are sent to the app. Google does not have access to the digital health network. What data does Google process when using ML Kit? Google only has access to technical information about the end device used and the general use of the additional function (e.g. error rate, camera settings) in order to record this statistically and thus improve the additional function. When you access, Google temporarily records the IP address of your end device. Information about you and the contents of the prescription will not be recorded by Google. Is the use of Google ML Kit voluntary? Yes. However, ML Kit is built into the prescription code scanner in the Android version of the e-prescription app. If you use the prescription code scanner on an Android device, the ML Kit function is also always used. However, you can do without using the prescription code scanner. Your prescriptions can also be loaded into the app if you register with the digital health network with the electronic health card or via your health insurance app. Can I see who has viewed my prescriptions? Yes. All access to your data is fully logged in the digital health network. In the e-prescription app you can see who has accessed your data. - Who can I contact if I have questions about the app or the e-prescription? - You can find detailed information in the data protection declaration. + Where can I contact if I have questions about the app or the e-prescription? + Detailed information can be found in the data protection declaration. Number of packs prescribed No prescriptions For this you need redeemable prescriptions. Select insurance company - Look for insurance + Search for insurance Cancel What would you like to apply for? - For this app you need a card and the associated PIN. + For this app you need a card and the corresponding PIN. How would you like to contact your insurance company? Your insurance company offers the following contact options - Your insurance company offers the following contact options + Your insurance company offers the following contact option Close PIN entered incorrectly. Access number entered incorrectly PUK entered incorrectly. - expense receipts - Show expense receipts - expense receipts - To receive expense receipts, you must be connected to the server. + cost receipts + View cost receipts + cost receipts + To receive cost receipts, you must be connected to the server. Connect - No expense receipts + No cost receipts Disable Cancel - disable function - This will delete all receipts from this device and from the server. - Receive expense receipts - Your cost receipts are also saved on the prescription server. - Receive + Deactivate function + This will delete all expense receipts from this device and the server. + Receive cost receipts + Your cost receipts are also saved on the recipe server. + Received Total: %s %s Select Split @@ -800,8 +799,8 @@ Submit %s € total price - Tip: Submit expense receipts via the insurance app - Submit cost receipts easily via your insurance company\'s app. In the next step, select this app and press Share. + Tip: Submit cost receipts via the insurance app + Submit cost receipts easily via your insurance company’s app. In the next step, select this app and press share. Practice Pharmacy Date @@ -809,38 +808,64 @@ Drug ID Issued for KVNR: %s - Date of birth: %s + Born on: %s OK - How do you submit receipts? - Transfer directly to the app of your insurance company/aid office. To do this, select the app on the next page. + How do you submit supporting documents? + Transfer directly to the app of your insurance/benefit office. To do this, select the app on the next page. or - Save the file and later import it into the insurance/aid portal. + Save the file and later import it into the insurance/benefit portal. Article: %s - Number: %s + Count: %s VAT: %s %% Gross price in EUR: %s - Additional Fees + Additional fees Emergency service fee BTM fee - T prescription fee - procurement costs + T-prescription fee + Procurement costs courier service Total in EUR: %s levy Really delete? - The file will be deleted from your device and from the server. + The file will be deleted from your device and the server. Delete Posted Postcode Place - Please enter your zip code to contact us. - Please enter your place of residence when contacting us. + Please provide your zip code to contact us. + Please indicate your place of residence to contact us. Will be redeemed for you Was redeemed for you You must be logged in to use this service. Insurance app Insurance card Associated PIN required + Can only be redeemed tomorrow as a self-payer + Only %s days left to redeem as self-payer + \nStill redeemable as a self-payer for %s days\n + Valid for %s days only + \nValid for %s days left\n + Only valid tomorrow + Charges apply + Takes insurance + The recipe(s) have been successfully transferred. + The recipe cannot be processed. Please try again. You may need to choose a different pharmacy. + The recipe cannot be processed. The pharmacy reports an unknown error. If necessary, try another pharmacy. + The prescription was rejected by the pharmacy. The prescription may be invalid or your delivery address or contact information may be invalid. + Unable to redeem, please check your internet connection. + The recipe was successfully transferred. However, the pharmacy reports a processing error. Please contact the pharmacy. + The prescription was rejected by the pharmacy. The prescription has already been redeemed. + The prescription was rejected by the pharmacy. The recipe has been deleted. + The recipe could not be transferred. Please check your internet connection and try again. + One or more recipes could not be transferred. + Error sending + Shipped successfully! + Error at the pharmacy + Error at the pharmacy + Contact pharmacy + Prescription already redeemed + Recipe deleted + No Internet To receive access logs, you must be connected to the server. You can still fill the prescription at a pharmacy within this period, but you will have to pay the entire purchase price for the medication yourself. Alternatively, you can ask your practice to have the prescription reissued. Done @@ -849,4 +874,13 @@ In the app Have this code scanned at your pharmacy. Billing correction request + Medicine + Please enter at least 1 character. + Or. Try the app in demo mode + Demo mode + Demo mode + Use demo mode + Demo mode enabled + End here + Activate demo mode diff --git a/android/src/main/res/values-en/strings_kbv_codes.xml b/app/features/src/main/res/values-en/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values-en/strings_kbv_codes.xml rename to app/features/src/main/res/values-en/strings_kbv_codes.xml diff --git a/android/src/main/res/values-fr/strings.xml b/app/features/src/main/res/values-fr/strings.xml similarity index 52% rename from android/src/main/res/values-fr/strings.xml rename to app/features/src/main/res/values-fr/strings.xml index ec3c9f9a..7cfd6f05 100644 --- a/android/src/main/res/values-fr/strings.xml +++ b/app/features/src/main/res/values-fr/strings.xml @@ -1,280 +1,280 @@ D\'ACCORD - Interrompre - Retour + Annuler + Dos autour Numérique. Rapide. Sécurisé. ID de tâche - code d\'accès + Code d\'accès Conditions d\'utilisation Protection des données - recettes + Recettes Accès à la caméra refusé Pour utiliser le scanner, vous devez autoriser l\'application à accéder à votre appareil photo dans les paramètres système. Concentrez la caméra sur un code de recette - Ceci n\'est pas un code de prescription valide + Ce n\'est pas un code de prescription valide Ce code de prescription a déjà été scanné - %s recette reconnue + Recette %s reconnue %s recettes reconnues - Interrompre - lumière de la caméra + Annuler + Lumière de la caméra Annuler la numérisation ? D\'ACCORD N\'annulez pas - Nous y voilà + Allons-y De quoi as-tu besoin: - Entrer le numéro d\'accès de la carte + Entrez le numéro d\'accès de la carte entrez le code PIN - essayer à nouveau + Essayer à nouveau Impossible de se connecter au serveur. - Il vous reste %s tentatives avant que votre carte ne soit bloquée. - Il vous reste %s tentatives avant que votre carte ne soit bloquée. + Il vous reste %s une tentative supplémentaire avant que votre carte ne soit bloquée. + Vous disposez %s tentatives supplémentaires avant que votre carte ne soit bloquée. - Vous trouverez le numéro d\'accès en haut à droite de votre carte santé. - Interrompre - Rechercher une carte... - Tenez la carte de santé à l\'arrière de votre appareil. + Vous trouverez le numéro d’accès en haut à droite de votre carte Santé. + Annuler + Recherche par carte… + Tenez la carte Santé contre le dos de votre appareil. Toujours à la recherche … Déplacez lentement la carte à l\'arrière de l\'appareil. Conseil - Les boîtiers d\'appareils peuvent rendre difficile la connexion via NFC. - carte reconnue + Les boîtiers des appareils peuvent rendre difficile la connexion via NFC. + Carte reconnue Essayez de ne pas déplacer la carte Santé. - Carnet de santé retrouvé. Veuillez ne pas bouger. + Carte de santé trouvée. S\'il vous plaît, ne bougez pas. connexion perdue - Tenez à nouveau votre carte de santé à l\'arrière de l\'appareil + Tenez à nouveau votre carte Santé contre le dos de l’appareil. Version : %s - Construire le hachage : %s - menu de débogage + Hachage de construction : %s + Menu Débogage Ouvert jusqu\'au %s Ouvert toute la journée imprimer éditeur - gematik GmbH\n Friedrichstraße 136\n 10117Berlin - Directeur général : Dr. médical Markus Leyck-Dieken\n Tribunal d\'enregistrement : tribunal de district de Berlin-Charlottenburg\n Numéro de registre du commerce : HRB 96351\n Numéro d\'identification de la taxe de vente : DE241843684 + Gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin + Directeur général : Dr. méd. Markus Leyck Dieken\n Tribunal d\'enregistrement : tribunal de grande instance de Berlin-Charlottenbourg\n Numéro de registre du commerce : HRB 96351\n Numéro d\'identification TVA : DE241843684 Responsable du contenu - docteur médical Markus Leyck-Dieken + Dr. méd. Markus Leyck Dieken Contact Avis - Nous nous efforçons d\'utiliser un langage non sexiste. Si vous remarquez des erreurs, nous attendons avec impatience de vous entendre par e-mail. - La plate-forme allemande moderne pour la médecine numérique - écrire un courrier - site Web ouvert + Nous nous efforçons d’utiliser un langage équitable entre les sexes. Si vous remarquez des erreurs, nous serions heureux de vous entendre par e-mail. + La plateforme moderne allemande pour la médecine numérique + Écrire un email + Ouvrir le site Web Accueillir Commencer l\'inscription - ouvrir - Enregistrer - Interrompre + Ouvrir + Registre + Annuler Sécurité - Juridique + Légal imprimer protection des données Conditions d\'utilisation - détails + Détails Marquer comme utilisé Marquer comme non utilisé - forme posologique - taille du paquet + Forme posologique + Taille du paquet Personne assurée Nom de famille adresse date de naissance - Assurance maladie / Payeurs + Assurance maladie/payeur statut - numéro d\'assurance + Numéro d\'assurance Prescripteur Nom de famille - Spécialiste médical + Médecin spécialiste Numéro de médecin (LANR) institution Nom de famille adresse - Numéro de local commercial - numéro de téléphone - adresse mail - accident du travail - jour de l\'accident - Numéro d\'entreprise ou d\'employeur de l\'accident - Voulez-vous supprimer définitivement cette recette ? - Éteindre - Interrompre + Numéro d\'usine + Numéro de téléphone + Adresse e-mail + Accident du travail + Jour de l\'accident + Numéro d\'entreprise ou d\'employeur accidenté + Souhaitez-vous supprimer définitivement cette recette ? + Supprimer + Annuler Horaires d\'ouvertures - site Internet - Remboursable uniquement aujourd\'hui en tant qu\'auto-payeur - Enregistrer + site web + Échangeable uniquement aujourd\'hui en tant qu\'auto-paieur + Registre Activer NFC - Veuillez activer la fonction NFC de votre appareil pour vous connecter avec votre carte de santé. + Veuillez activer la fonction NFC de votre appareil pour vous connecter avec votre carte Santé. Activer Correct Des ordonnances rachetées ? - Voulez-vous marquer les ordonnances comme remboursées ? - Non échangé + Souhaitez-vous marquer les ordonnances comme utilisées ? + Non racheté Racheté - Ouvre à %s + Ouvre à %s heure +49 800 277 377 7 Hotline technique - Ouvrir le scanner pour les recettes - Idées + Scanner ouvert pour les ordonnances + Paramètres Supprimer les captures d\'écran - Empêche l\'affichage d\'une vignette lors du changement d\'application - Autorisez-vous e-recipe à analyser votre comportement d\'utilisation de manière anonyme ? + Empêche l\'affichage d\'une image d\'aperçu lors du changement d\'application + Autorisez-vous E-Prescription à analyser votre comportement d’utilisation de manière anonyme ? Informations techniques Sécurité de vos données de prescription - Veuillez vous assurer que les personnes avec qui vous partagez cet appareil et dont les caractéristiques biométriques peuvent être stockées sur cet appareil ont également accès à vos ordonnances. + Veuillez vous assurer que les personnes avec lesquelles vous pouvez partager cet appareil et dont les caractéristiques biométriques peuvent être stockées sur cet appareil ont également accès à vos ordonnances. Envoi échoué Aucun programme de messagerie configuré Aucun résultat Nous n\'avons trouvé aucun résultat pour ce terme de recherche. - Licences Open Source + Licences open source Contact - Appeler la hotline technique - Participer au sondage + Appelez la hotline technique + Participez à l\'enquête +49 800 277 377 7 - Je veux aider à améliorer cette application - Cela inclut les informations matérielles et logicielles sur votre téléphone, les paramètres de l\'application d\'e-prescription et la quantité d\'utilisation, mais jamais de données sur vous ou votre santé. - Les données ne sont mises à la disposition de gematik GmbH que par le responsable du traitement et sont supprimées au plus tard au bout de 180 jours. Vous pouvez désactiver à nouveau l\'analyse à tout moment dans le menu de l\'application. - Ces données nous permettent de comprendre quelles fonctions sont fréquemment utilisées et de les améliorer. De plus, nous pouvons estimer combien de temps une technologie plus ancienne doit être prise en charge et quand nous pouvons, par exemple, rendre obligatoire une version plus récente du système d\'exploitation sans affecter (trop) d\'utilisateurs. - améliorer l\'application + Je veux contribuer à améliorer cette application + Cela inclut les informations matérielles et logicielles sur votre téléphone, les paramètres de l\'application de prescription électronique et l\'étendue de son utilisation, mais jamais les données sur vous ou votre santé. + Les données ne seront mises à la disposition de gematik GmbH que par le responsable du traitement des données et seront supprimées au plus tard au bout de 180 jours. Vous pouvez désactiver l\'analyse à tout moment dans le menu de l\'application. + Ces données nous permettent de comprendre quelles fonctions sont fréquemment utilisées et de les améliorer. Nous pouvons également estimer combien de temps une technologie plus ancienne doit être prise en charge et quand nous pouvons, par exemple, rendre obligatoire une version plus récente du système d\'exploitation sans affecter (trop) d\'utilisateurs. + Améliorer l\'application L\'analyse anonyme reste désactivée %s Merci pour votre soutien ! - Enregistrer - Merci de vous identifier pour télécharger les recettes. - Remarque pour les pharmacies : nous obtenons les coordonnées et les informations sur les pharmacies sur mein-apothekenportal.de de l\'Association allemande des pharmacies. Avez-vous découvert une erreur ou souhaitez-vous corriger des données ? + Registre + Veuillez vous identifier pour télécharger des recettes. + Remarque pour les pharmacies : Nous obtenons les coordonnées et les informations sur les pharmacies auprès de mein-apothekenportal.de de l\'Association allemande des pharmaciens. Avez-vous découvert une erreur ou souhaitez-vous corriger des données ? Apprendre encore plus - pharmacies + Pharmacies Malheureusement, cela n\'a pas fonctionné \uD83D\uDE15 Veuillez réessayer. Entrer le mot de passe Plus loin Accessibilité Zoom - Permet d\'agrandir l\'application en pinçant ou en écartant les doigts (pincer pour zoomer). + Vous permet d\'agrandir l\'application en pinçant pour zoomer. mot de passe Sécurisez vos données avec un mot de passe de votre choix. mot de passe - Enregistrer sur ordinateur - montrer le mot de passe + Sauvegarder + Montrer le mot de passe Répéter le mot de passe Recommandations : %s - écrire un courrier - Lorsque vous envoyez votre message, les informations suivantes sur le matériel et le système d\'exploitation utilisés seront transmises : - Échange sur place uniquement - Vous ne pouvez pas encore envoyer d\'ordonnances électroniques à cette pharmacie. + Écrire un email + Lorsque vous envoyez votre message, les informations suivantes sur le matériel et le système d\'exploitation utilisé sont transmises : + Échangez sur place uniquement + Vous ne pouvez pas encore envoyer d\'e-prescriptions à cette pharmacie. Actuellement ouvert - service de messagerie + Service de messagerie Expédition filtre Filtre Aucun emplacement disponible Compris - Mots de passe répétés + Correspondances de mot de passe répétées Erreur 20 10 76631 - Votre certificat de carte de santé est invalide. Votre carte a-t-elle expiré ? Veuillez contacter votre assurance maladie. + Votre certificat de carte santé n’est pas valide. Peut-être que votre carte est expirée ? Veuillez contacter votre caisse d\'assurance maladie. Tentatives de connexion infructueuses %s tentatives de connexion infructueuses ont été détectées. %s tentatives de connexion infructueuses ont été détectées. Choisissez la meilleure sauvegarde de périphérique - Il peut s\'agir d\'une empreinte digitale, d\'un motif de balayage ou similaire - jetons - jeton d\'accès + Il peut s\'agir d\'une empreinte digitale, d\'un motif de balayage ou de quelque chose de similaire + Jetons + Jetons d\'accès Jetons SSO Aucun jeton d\'accès disponible aucun jeton SSO disponible copié dans le presse-papiers Cliquez pour copier le jeton dans le presse-papiers - Valable aujourd\'hui seulement + Uniquement valable aujourd\'hui Permettre pas de connexion au serveur S\'il vous plait, réessayez dans quelques minutes Charger à nouveau - afficher les jetons - Comment souhaitez-vous sécuriser l\'application ? + Afficher les jetons + Comment souhaitez-vous sécuriser l’application ? Avis Aucune sauvegarde d\'appareil n\'a été configurée pour cet appareil - Nous vous recommandons de protéger en outre vos données médicales avec la sécurité de l\'appareil, comme un code d\'accès ou la biométrie. - Ne plus afficher cet avis à l\'avenir. + Nous vous recommandons de protéger également vos informations médicales avec des dispositifs de sécurité tels qu\'un code ou des données biométriques. + N\'affichez plus cet avis à l\'avenir. La connexion a échoué. Une connexion réseau n\'a pas pu être établie. Échec de la communication avec le serveur : code d\'état %s . - Impossible de communiquer avec le serveur : veuillez vérifier la connexion Internet et les paramètres d\'heure/date. + Échec de la communication avec le serveur : veuillez vérifier la connexion Internet et les paramètres d’heure/date. avertissement Votre appareil peut avoir une sécurité réduite - Cela peut être causé, par exemple, par des appareils manipulés ou un mode développeur activé. Pour des raisons de sécurité, nous vous déconseillons d\'utiliser l\'application sur des appareils jailbreakés. - Je reconnais le risque accru et je veux continuer. - Pourquoi les appareils avec accès root représentent-ils un risque potentiel pour la sécurité ? + Cela peut être dû, par exemple, à des appareils manipulés ou à l\'activation du mode développeur. Nous vous recommandons de ne pas utiliser l\'application sur des appareils jailbreakés pour des raisons de sécurité. + Je reconnais le risque accru et j’aimerais quand même continuer. + Pourquoi les appareils avec accès root constituent-ils un risque de sécurité potentiel ? Apprendre encore plus https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html - Nom de profil - Veuillez entrer un nom pour le nouveau profil. - nom de profil - profils - Comment reconnaître une carte de santé compatible NFC + Nom du profil + Veuillez saisir un nom pour le nouveau profil. + Nom de profil + Profils + Comment reconnaître une carte Santé compatible NFC Aucun contact possible via cette application - Veuillez utiliser les canaux habituels pour contacter votre compagnie d\'assurance. + Veuillez utiliser les canaux habituels pour contacter votre compagnie d’assurance. Carte Santé et NIP - NIP uniquement + Code PIN uniquement Inscription dans l\'application e-prescription Le champ du nom ne peut pas être vide. - Un profil avec le nom saisi existe déjà. + Un profil portant le nom que vous avez saisi existe déjà. profil %s sélectionné Couleur de l\'arrière plan - gris printemps - droséra + Gris printanier + Droséra Il! Est! Rose! Arbre - Lune bleue de septembre + Lune bleue septembre Pas connecté - Liés ensemble + Attachés ensemble Dernière connexion le %s Supprimer le profil? - Cela effacera toutes les données de profil sur cet appareil. Vos ordonnances dans le réseau de la santé resteront intactes. - Éteindre - Interrompre + Cela supprimera toutes les données du profil sur cet appareil. Vos ordonnances dans le réseau de la santé seront conservées. + Supprimer + Annuler Supprimer le profil Vous souhaitez supprimer le dernier profil. - L\'application nécessite au moins un profil. Veuillez entrer un nom pour le nouveau profil. + L\'application nécessite au moins un profil. Veuillez saisir un nom pour le nouveau profil. Erreur 20 10 76831 - Le répertoire des cartes de santé n\'a pas pu être atteint. Veuillez réessayer. - Vous pouvez trouver des informations vérifiées par des experts sur les maladies, les codes CIM et sur les questions de prévention et de soins sur le portail national de la santé. - Ouvrez Gesund.bund.de + Le répertoire de la carte santé n\'a pas pu être atteint. Veuillez réessayer. + Vous pouvez trouver des informations vérifiées par des experts sur les maladies, les codes CIM et les sujets de prévention et de soins sur le portail national de la santé. + Ouvrir healthy.bund.de Nous avons modifié la politique de confidentialité - L\'application e-prescription a évolué. Cela a rendu nécessaire la mise à jour de notre politique de confidentialité. + L’application e-prescription a évolué. Cela a rendu nécessaire la mise à jour de notre politique de confidentialité. Ouvrir la politique de confidentialité - Cela a changé depuis le %s : - Que se passe-t-il lorsque vous ouvrez l\'application ? - Que se passe-t-il si j\'utilise la fonction appareil photo / lis des recettes avec l\'appareil photo ? + Cela a changé depuis %s : + Que se passe-t-il lorsque vous ouvrez l\'application ? + Que se passe-t-il si j\'utilise la fonction appareil photo / lis des recettes avec l\'appareil photo ? Aucune nouvelle recette disponible - %s nouvelle recette + La nouvelle recette %s %s nouvelles recettes Rachetable - En rachat + En rédemption Racheté Inconnu Afficher les journaux d\'accès - Qui a accédé à vos recettes et quand ? + Qui a accédé à vos recettes et quand ? Clé d\'accès au service de prescription - journaux d\'accès + Journaux d\'accès Aucun journal d\'accès Il n\'y a pas encore de journaux d\'accès. La recette est actuellement en cours et ne peut pas être supprimée Accepter - Apparemment ça n\'a pas marché - Nous sommes conscients que le lien avec la carte de santé a ses pièges. À l\'avenir, l\'inscription devrait donc également être possible via une application d\'assurance maladie déjà authentifiée. \n\n Nous nous efforçons également de permettre aux ordonnances d\'être échangées numériquement sans enregistrement. \n\n Avez-vous remarqué quelque chose au cours de ce processus que vous aimeriez partager avec nous ? Veuillez nous écrire, nous sommes également heureux de recevoir des commentaires très critiques. + Apparemment, ça n\'a pas fonctionné + Nous sommes conscients que le lien avec la carte santé comporte ses pièges. À l’avenir, l’inscription devrait également être possible via une application d’assurance maladie déjà authentifiée. \n\n Nous veillons également à ce que les ordonnances puissent être échangées numériquement sans inscription. \n\n Avez-vous remarqué quelque chose au cours de ce processus que vous aimeriez partager avec nous ? N\'hésitez pas à nous écrire, nous serions également heureux de recevoir des commentaires très critiques. Conseils de connexion Améliorer la force de la connexion Si nécessaire, retirez le capot de protection. - Si l\'appareil vibre puis rompt la connexion, recherchez la position optimale dans un petit rayon. - Déplacez l\'appareil sur la carte très lentement. + Si l\'appareil vibre et que la connexion se rompt, recherchez la position optimale dans un petit rayon. + Déplacez l\'appareil très lentement sur la carte. Placez l\'appareil directement sur la carte. - Pour ce faire, placez la carte de santé sur une surface plane (par exemple une table). + Pour ce faire, placez la carte Santé sur une surface plane (par exemple une table). Améliorer la force de la connexion Notez l\'emplacement du capteur NFC Découvrez où se trouve le capteur NFC dans votre appareil (ici, par exemple, un aperçu des appareils de %s ). @@ -282,60 +282,60 @@ Prochain conseil Plus loin Fermer - Expérimenter + Essayer Écrivez-nous - Permis de recherche en pharmacie - racheter - Ordonnance scannée - Scanné le %s - Marqué comme échangé le %s - Comment voulez-vous continuer ? + Recherche de pharmacie sous licence + Racheter + Recette numérisée + Scanné sur %s + Marqué comme utilisé le %s + Comment veux-tu continuer ? Commande Bientôt disponible - Réservez maintenant pour la collecte ou faites-vous livrer par service de messagerie ou par expédition - Enregistrer pour une commande ultérieure - Enregistrer les recettes sur l\'appareil + Réservez maintenant pour la collecte ou faites-vous livrer par coursier ou par expédition + Sauvegarder pour une commande ultérieure + Enregistrer des recettes sur l\'appareil Continuer avec la recette %s Continuer avec %s recettes - Impossible de connecter la carte de santé - Le profil actuel est déjà connecté à une autre carte santé (numéro d\'assurance maladie %s ). - Votre carte santé est déjà reliée à un autre profil. Passez au profil %s . - Enregistrer sur ordinateur - coordonnées et adresse + Échec de la connexion à la carte Santé + Le profil actuel est déjà lié à une autre carte santé (numéro d\'assurance maladie %s ). + Votre carte Santé est déjà liée à un autre profil. Accédez au profil %s . + Sauvegarder + Coordonnées et adresse Contact - numéro de téléphone - Veuillez fournir un numéro de téléphone pour le contact. + Numéro de téléphone + Veuillez fournir un numéro de téléphone pour nous contacter. Adresse e-mail (facultatif) adresse de livraison - prénom et nom - Veuillez saisir un nom et un prénom à des fins de contact. + Prénom et nom + Veuillez indiquer votre nom et prénom pour nous contacter. Rue et numéro de maison - Veuillez entrer une rue et un numéro de maison afin que nous puissions être contactés. + Veuillez fournir une rue et un numéro de maison pour nous contacter. Adresse supplémentaire (facultatif) Instructions de livraison (facultatif) - Coordonnées supplémentaires requises + Autres coordonnées requises Annuler les modifications? jeter - Pour la recherche, l\'annuaire des pharmacies utilise des coordonnées géographiques qui ont été déterminées à l\'aide d\'OpenStreetMap. Nous remercions le projet pour cette aide. - © OpenStreetMap ( %s ) + Pour la recherche, l\'annuaire des pharmacies utilise des coordonnées géographiques déterminées à l\'aide d\'OpenStreetMap. Nous remercions le projet pour cette aide. + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Confidentialité et utilisation + Protection et utilisation des données Plus loin - Vous avez reçu votre NIP dans une lettre de votre compagnie d\'assurance maladie. + Vous avez reçu votre code PIN dans une lettre de votre caisse d\'assurance maladie. Aucun code PIN reçu code PIN - Vérifiez la connexion à Internet et le réglage de l\'heure et de la date de votre appareil. + Vérifiez la connexion Internet et les paramètres d\'heure et de date de votre appareil. Pour vous connecter, appuyez sur « Déverrouiller ». - enfermé dehors? Veuillez vérifier vos identifiants biométriques sur cet appareil. + Enfermé dehors? Veuillez vérifier vos informations d\'identification biométriques sur cet appareil. Mot de passe oublié? Veuillez supprimer l\'application, puis la réinstaller. Vous pouvez découvrir pourquoi dans notre %s . zone d\'aide - taille de l\'emballage et unité + Taille du conditionnement et unité ingrédient actif - Quantité d\'ingrédient actif - désignation du lot - Exp + Quantité de principe actif + Nom du lot + Exp. catégorie Vaccin Accepter @@ -350,145 +350,145 @@ Le mot de passe n\'est pas visible biométrie mot de passe - attendre une réponse - Aucune ordonnance - Vous n\'avez actuellement aucune ordonnance remboursable. + Attendre une réponse + Aucune recette + Vous n’avez actuellement aucune ordonnance remboursable. Mettre à jour Déconnexion automatique - Pour des raisons de sécurité, la connexion au serveur de recettes est interrompue au bout de 12 heures. Reconnectez-vous pour obtenir les recettes actuelles. + Pour des raisons de sécurité, la connexion au serveur de recettes est coupée au bout de 12 heures. Reconnectez-vous pour obtenir les recettes actuelles. Connecter - Avez-vous reçu une copie papier? + Avez-vous reçu une impression papier ? Ajoutez des recettes à votre liste en appuyant sur le bouton de numérisation dans le coin supérieur droit. - Numériser l\'impression papier - Vous devez être connecté pour recevoir les recettes automatiquement. - Enregistrer - Aucune ordonnance remboursée + Scanner une impression papier + Pour recevoir automatiquement des ordonnances, vous devez être connecté. + Registre + Aucune ordonnance rachetée Vos ordonnances échangées sont affichées ici. Pour des raisons de protection des données, vos recettes seront supprimées du serveur de recettes après 100 jours. - Aucune ordonnance remboursée - Vos ordonnances échangées sont affichées ici. Ajoutez des ordonnances via scan pour commencer à racheter. - gestion d\'appareils + Aucune ordonnance rachetée + Vos ordonnances échangées sont affichées ici. Ajoutez des recettes via scan pour commencer à échanger. + Gestion d\'appareils Des appareils connectés - Inscrit depuis %s (ce périphérique) + Inscrit depuis %s (cet appareil) Inscrit depuis %s - Pour des raisons de sécurité, la connexion au serveur de recettes est interrompue au bout de 12 heures. Pour vous reconnecter, vous avez besoin de votre carte Santé et de votre NIP pour chaque processus de connexion. + Pour des raisons de sécurité, la connexion au serveur de recettes est coupée au bout de 12 heures. Pour vous reconnecter, vous aurez besoin d’une carte Santé et d’un code PIN pour chaque processus de connexion. code PIN - Entrez votre NIP (carte Santé). + Entrez le code PIN (carte Santé). Plus loin - Enregistrer + Registre Des appareils connectés - enlevez l\'appareil? - Interrompre - Supprimé + Enlevez l\'appareil? + Annuler + Retirer Supprimer cet appareil ? - Voulez-vous supprimer %s ? - Si vous supprimez %s , la connexion au serveur de recettes sera définitivement déconnectée dans 12 heures au plus tard. - Les appareils sont en cours de chargement... + Voulez-vous supprimer %s ? + Si vous supprimez %s , la connexion au serveur de recettes sera définitivement déconnectée au plus tard dans 12 heures. + Les appareils se chargent… Aucun appareil - Aucun appareil n\'est connecté à cette carte Santé. + Aucun appareil n’est connecté à cette carte Santé. Essayer à nouveau - Ah ah :-( + Euh oh :-( La liste des appareils n\'a pas pu être chargée. wwweg… Pas de connexion Internet. Médicaments et pansements - stupéfiants - Livraison de médicaments sur ordonnance selon § 4 AMVV + Stupéfiants + Délivrance de médicaments sur ordonnance conformément à l\'article 4 AMVV As-tu besoin d\'aide? - Nous avons rassemblé pour vous quelques conseils pour résoudre les problèmes les plus courants. - Commencer les conseils de connexion - ouvrir + Nous avons rassemblé pour vous quelques conseils afin de résoudre les problèmes les plus courants. + Démarrer les conseils de connexion + Ouvrir carte bloquée - Le code PIN a été saisi incorrectement trois fois. Votre carte a donc été bloquée pour des raisons de sécurité. - déverrouiller la carte - Entrez le code PUK - Avec votre code PIN, vous avez reçu un code PUK à 8 chiffres de votre compagnie d\'assurance. - Choisir un nouveau NIP - Vous pouvez choisir vous-même votre nouveau numéro d\'identification personnel (NIP) (6 à 8 chiffres). - NIP mémorisé ? - Veuillez noter votre code PIN et le conserver en lieu sûr. - Interrompre + Le code PIN a été saisi incorrectement à trois reprises. Votre carte a donc été bloquée pour des raisons de sécurité. + Débloquer la carte + Entrez PUK + Avec votre code PIN, vous avez reçu un PUK à 8 chiffres de votre compagnie d\'assurance. + Choisir un nouveau code PIN + Vous pouvez choisir vous-même votre nouveau numéro d\'identification personnel (PIN) (6 à 8 chiffres). + Vous vous souvenez de votre code PIN ? + Veuillez noter votre code PIN et le conserver dans un endroit sûr. + Annuler D\'ACCORD Déverrouillage impossible - Vous avez atteint le nombre maximum de cartes déverrouillées avec ce code PUK ou vous l\'avez saisi de manière incorrecte à plusieurs reprises. Veuillez contacter votre compagnie d\'assurance. + Vous avez atteint le nombre maximum de déverrouillages de carte avec ce PUK ou vous l\'avez saisi de manière incorrecte à plusieurs reprises. Veuillez contacter votre compagnie d\'assurance. Vous pouvez utiliser un PUK pour un maximum de 10 déverrouillages. - carte déverrouillée + Carte débloquée De quoi as-tu besoin: - votre carte de santé + Votre carte santé PUK de votre carte santé Plus loin - carte de santé - Commander un NIP ou une carte - Enregistrer + Carte de santé + Commander un code PIN ou une carte + Registre Comment voulez-vous vous connecter ? Carte de santé compatible NFC - NIP pour la carte santé - Vous n\'avez pas encore de carte de santé et de code PIN compatibles NFC ? + Code PIN pour la carte Santé + Vous n\'avez pas encore de carte Santé ni de code PIN compatible NFC ? Appliquer maintenant - Ou : Connectez-vous avec le %s . + Ou : Connectez-vous avec %s . Votre application d\'assurance maladie - "Votre numéro d\'accès se trouve dans le coin supérieur droit de votre carte Santé." + « Vous trouverez votre numéro d\'accès dans le coin supérieur droit de votre carte Santé. » Ma carte n\'a pas de numéro d\'accès - Il vous reste %s tentatives avant que votre carte ne soit bloquée. - Il vous reste %s tentatives avant que votre carte ne soit bloquée. + Il vous reste %s une tentative supplémentaire avant que votre carte ne soit bloquée. + Vous disposez %s tentatives supplémentaires avant que votre carte ne soit bloquée. - Mettez la carte de santé au dos du téléphone + Placez la carte Santé au dos du téléphone Le processus suivant peut prendre jusqu\'à 30 secondes. Placez la carte %s au dos du téléphone. - dans le coin supérieur droit - au milieu supérieur - en haut à gauche - au milieu à droite + dans la zone supérieure droite + au milieu dans la zone supérieure + dans la zone supérieure gauche + dans la zone médiane à droite milieu - au centre gauche + dans la zone centrale à gauche dans la zone inférieure droite - en bas au centre - en bas à gauche - Aider + au milieu dans la zone inférieure + dans la zone inférieure gauche + Aide Envoyé il y a %s minutes Envoyé le %s Envoyé à l\'instant - Envoyé à %s heures + Envoyé à %s heure N\'est plus valide - Connectez-vous avec l\'application - choisir une assurance - Vous n\'avez pas trouvé ce que vous cherchiez ? Cette liste est constamment élargie. L\'inscription avec une carte de santé est déjà prise en charge par toutes les caisses d\'assurance maladie. - Retour d\'expérience de l\'application e-prescription - Nous attendons vos commentaires avec impatience. Veuillez utiliser l\'espace ci-dessous et être aussi précis que possible : + Inscrivez-vous avec l\'application + Choisissez une assurance + Vous n\'avez pas trouvé ce que vous cherchiez ? Cette liste est constamment élargie. L\'inscription avec une carte de santé est déjà prise en charge par chaque caisse d\'assurance maladie. + Retour d’expérience de l’application e-prescription + Nous attendons avec impatience vos commentaires. Veuillez utiliser l\'espace suivant et être aussi précis que possible : PUK Fermer Quel dommage… - Malheureusement, votre appareil ne répond pas aux exigences minimales pour vous connecter à l\'application e-prescription. Au moins Android 7 et une puce NFC sont nécessaires pour une authentification sécurisée avec votre carte de santé. + Malheureusement, votre appareil ne répond pas aux exigences minimales pour s\'inscrire dans l\'application e-prescription. Pour une authentification sécurisée avec votre carte Santé, au minimum Android 7 et une puce NFC sont requis. Apprendre encore plus Enregistrer les données de connexion ? - Enregistrer sur ordinateur - Ne sauvegardez pas + Sauvegarder + Ne sauvegarde pas Avis - Pour des raisons de sécurité, la connexion au serveur de recettes est interrompue au bout de 12 heures. Pour vous reconnecter, vous avez besoin de votre carte Santé et de votre NIP pour chaque processus de connexion. - Configurer la sécurité biométrique - Enregistrement des données d\'accès impossible. Configurez au préalable la sécurité biométrique (par exemple, empreinte digitale) sur votre appareil. - Interrompre - Idées + Pour des raisons de sécurité, la connexion au serveur de recettes est coupée au bout de 12 heures. Pour vous reconnecter, vous aurez besoin d’une carte Santé et d’un code PIN pour chaque processus de connexion. + Mettre en place la sécurité biométrique + Il n\'est pas possible de sauvegarder les données d\'accès. Au préalable, configurez la sécurité biométrique (par exemple empreinte digitale) sur votre appareil. + Annuler + Paramètres Avis Accepter Sécurité de vos données de prescription - \"Cette application utilise le capteur biométrique le plus sécurisé fourni par votre appareil pour stocker vos informations d\'identification dans une zone sécurisée de la mémoire de l\'appareil.\" - La sécurité biométrique de vos données d\'accès vous permet d\'ouvrir cette application à l\'avenir sans entrer votre code PIN et votre carte de santé, et de consulter, appeler, échanger ou supprimer des ordonnances. - Veuillez vous assurer que les personnes avec qui vous partagez cet appareil et dont les caractéristiques biométriques peuvent être stockées sur cet appareil ont également accès à vos ordonnances. - ça n\'a malheureusement pas fonctionné - L\'authentification avec l\'application d\'assurance maladie a échoué. + \"Cette application utilise le capteur biométrique le plus sécurisé fourni par votre appareil pour sécuriser vos informations d\'identification dans une zone protégée du stockage de l\'appareil.\" + La sécurité biométrique de vos données d\'accès vous permet d\'ouvrir cette application à l\'avenir, de consulter, de récupérer, d\'utiliser ou de supprimer des ordonnances sans carte de santé et sans saisir votre code PIN. + Veuillez vous assurer que les personnes avec lesquelles vous pouvez partager cet appareil et dont les caractéristiques biométriques peuvent être stockées sur cet appareil ont également accès à vos ordonnances. + cela n\'a malheureusement pas fonctionné + L\'authentification auprès de l\'application d\'assurance maladie n\'a pas abouti. Expiré le %s La recette a déjà été supprimée du serveur - Veuillez corriger votre saisie ou annuler les modifications + Veuillez corriger votre entrée ou annuler les modifications Correct - données assurées + Données de l\'assuré Nom de famille Assurance - numéro d\'assurance - numéro d\'accès à la carte - Enregistrer - Se désinscrire - Enregistrer sur ordinateur + Numéro d\'assurance + Numéro d\'accès à la carte + Registre + Se déconnecter + Sauvegarder Changement Éditer la photo de profil Plus loin @@ -496,27 +496,27 @@ Veuillez réessayer plus tard. Essayer à nouveau Rechercher une assurance - Se connecter au serveur de recettes maintenant ? + Vous connecter au serveur de recettes maintenant ? Connecté avec succès connexion perdue - Se connecter au serveur de recettes maintenant ? + Vous connecter au serveur de recettes maintenant ? Pas de jetons - Vous recevrez un jeton lorsque vous serez connecté au service de prescription.\n - ordres + Vous recevrez un jeton lorsque vous serez connecté au service de prescription.\n + Ordres Sélectionnez le code PIN souhaité - déverrouiller la carte - Choisissez NIP - Répéter le NIP + Débloquer la carte + Sélectionnez le code PIN + Répéter le code PIN Les entrées diffèrent les unes des autres. Aucune commande Vous n\'avez pas encore de commandes. - Juste maintenant - A %s heures + Tout à l\' heure + À %s heures Le panier est prêt - La recette a été ajoutée à votre panier. Veuillez vous rendre sur le site Web de la pharmacie pour terminer la commande. + La recette a été ajoutée à votre panier. Veuillez vous rendre sur le site Web de la pharmacie pour finaliser la commande. Ouvrir le panier - Présentez ce code de retrait à la pharmacie. - Recevoir le code de prise en charge + Présentez ce code de collecte à la pharmacie. + Code de retrait reçu Le message ne peut pas s\'afficher Veuillez contacter votre pharmacie ( %s ). Afficher le lien du panier @@ -527,211 +527,210 @@ Aperçu de la commande Nouveau Cours - Commande - Gratuit pour l\'appelant. Horaires de service : du lundi au vendredi de 8h00 à 20h00 sauf jours fériés + L\'ordre + Gratuit pour l\'appelant. Horaires de service : du lundi au vendredi de 8h00 à 20h00 sauf les jours fériés Pharmacie Sélectionnez le code PIN souhaité - PIN souhaité enregistré + Code PIN souhaité enregistré Actuellement ouvert et près de chez moi Filtrer par … lancer la recherche - affectation directe - pharmacies + Affectation directe + Pharmacies Numéro de téléphone (facultatif) - Recherche par nom ou adresse + Rechercher par nom ou adresse Aucune information pharmaceutique valide Aucune information actuelle n\'a été trouvée sur cette pharmacie. L\'entrée pour cette pharmacie sera supprimée. D\'ACCORD - Répertoire des pharmacies non disponible - Actuellement, aucune information actuelle sur cette pharmacie ne peut être récupérée. S\'il vous plait, vérifiez votre connexion internet. - Interrompre + Annuaire des pharmacies non disponible + Actuellement, aucune information actuelle sur cette pharmacie n\'est accessible. S\'il vous plait, vérifiez votre connexion internet. + Annuler Essayer à nouveau - Enregistrer l\'environnement + Sauver l\'environnement La connexion n\'est pas possible - Il semble que vos identifiants de connexion biométriques aient changé. Veuillez vous réinscrire avec votre carte santé. - Interrompre - Enregistrer - profil 1 + Il semble que vos caractéristiques de connexion biométriques aient changé. Veuillez vous reconnecter avec votre carte Santé. + Annuler + Registre + Profil 1 Près de moi - Remboursable plus tard - Rachetable à partir de %s - améliorations du produit + Échangeable plus tard + Échangeable à partir de %s + Améliorations du produit Analyse anonyme - Aidez-nous à améliorer cette application. Toutes les données d\'utilisation sont collectées de manière anonyme et ne sont utilisées que pour améliorer l\'expérience utilisateur. - sécurité de l\'appareil + Aidez-nous à améliorer cette application. Toutes les données d\'utilisation sont collectées de manière anonyme et sont utilisées exclusivement pour améliorer l\'expérience utilisateur. + Sécurité des appareils paramètres personnels Accessibilité - améliorations du produit + Améliorations du produit Recette ajoutée Recette déjà disponible Une erreur s\'est produite lors de l\'importation - Éteindre - Ordonnance scannée - Remplacement possible - NIP oublié + Supprimer + Recette numérisée + Préparation de remplacement possible + Code PIN oublié %s Recette %s Recettes - J\'ai lu et j\'accepte la politique de confidentialité et les conditions d\'utilisation. + J\'ai lu et accepté la politique de confidentialité et les conditions d\'utilisation. Protection des données Conditions d\'utilisation Nous voudrions: Améliorer la convivialité. - Détecter les erreurs et les plantages. - Toutes les données sont bien sûr collectées de manière anonyme. - Vous pouvez modifier cette décision dans les paramètres système à tout moment. + Détectez les erreurs et les plantages. + Toutes les données sont bien entendu collectées de manière anonyme. + Vous pouvez modifier cette décision à tout moment dans les paramètres système. Continuer Accepter - Cette application utilise la méthode la plus sécurisée fournie par votre appareil. - Enregistrer sur ordinateur + Cette application utilise la méthode la plus sûre fournie par votre appareil. + Sauvegarder Choisir médicament - nom commercial + Nom commercial Oui Non dosage date d\'émission - Cette ordonnance vous sera rachetée dans le cadre d\'un traitement. + Cette ordonnance vous sera échangée dans le cadre d\'un traitement. Non spécifié - paiement supplémentaire + Paiement supplémentaire médicament - Bons de livraison - Admissible selon LPP - préparation alternative - nom de la recette + Instructions de soumission + Eligible selon LPP + Préparation alternative + Nom de la recette Emballage - instruction d\'artisanat + Instructions de fabrication Description donné par publié le: ingrédient actif - prescrit + Prescrit Recevoir Qu\'est-ce qu\'une mission directe ? - Dans le cas des références directes, une ordonnance d\'un cabinet ou d\'un hôpital est échangée directement en pharmacie. Les assurés n\'ont aucune démarche à entreprendre et ne peuvent intervenir dans le processus de rachat. \n\n Les références directes sont répertoriées dans l\'application e-prescription pour rendre votre traitement plus transparent pour vous. - frais de service d\'urgence - Il faut parfois se dépêcher. Certaines ordonnances peuvent être échangées sans le paiement supplémentaire de frais de service d\'urgence, comme la nuit ou les jours fériés. - Médicaments soumis à co-paiement - Exonéré de co-paiement - Les titulaires d\'une assurance maladie légale doivent payer une participation pouvant aller jusqu\'à dix euros pour les médicaments sur ordonnance. \n\n Le montant de la quote-part dépend du prix de vos médicaments. Vous devez payer vous-même les médicaments qui coûtent moins de 5 €.\n Pour les médicaments plus chers, vous devez payer 10 % du prix, mais au moins 5 € et au maximum 10 €. \n\n Les enfants et les jeunes de moins de 18 ans sont généralement exemptés du ticket modérateur. \n\n Si vos frais annuels de médicaments dépassent votre limite financière, vous pouvez être exempté du ticket modérateur. Parlez-en à votre mutuelle. - Vous êtes exonéré du co-paiement de ce médicament. Votre assurance maladie prendra en charge le coût des médicaments. - Combien de temps cette ordonnance est-elle valable ? + Avec référence directe, une ordonnance d\'un cabinet ou d\'un hôpital est exécutée directement en pharmacie. Les assurés n’ont aucune démarche à effectuer et ne peuvent intervenir dans le processus de rachat. \n\n Les références directes sont répertoriées dans l\'application de prescription électronique pour rendre votre traitement plus transparent pour vous. + Frais de service d\'urgence + Il faut parfois se dépêcher. Certaines ordonnances peuvent être exécutées sans le paiement supplémentaire de frais de service d\'urgence, par exemple la nuit ou les jours fériés. + Médicaments soumis au ticket modérateur + Exonéré de paiement supplémentaire + Les personnes bénéficiant d\'une assurance maladie légale doivent payer un supplément pouvant aller jusqu\'à dix euros pour les médicaments sur ordonnance. \n\n Le montant du supplément dépend du prix de vos médicaments. Vous devez payer vous-même les médicaments qui coûtent moins de 5 €.\n Pour les médicaments plus chers, vous devez payer dix pour cent du prix, mais au moins 5 € et au maximum 10 €. \n\n Les enfants et les jeunes de moins de 18 ans sont généralement exonérés du paiement supplémentaire. \n\n Si vos frais annuels de médicaments dépassent votre plafond de charge financière, vous pouvez être exonéré du ticket modérateur. Parlez-en à votre caisse d’assurance maladie. + Vous êtes exonéré du paiement d’une quote-part pour ce médicament. Votre compagnie d’assurance maladie prendra en charge le coût des médicaments. + Quelle est la durée de validité de cette prescription ? Pendant cette période, vous pouvez faire racheter votre ordonnance dans n\'importe quelle pharmacie moyennant un supplément de 10 € maximum. - Remplacement possible - En raison des exigences légales de votre caisse maladie, une alternative avec le même principe actif peut vous être proposée. \n\n Les médicaments peuvent avoir une apparence et un nom différents, avoir des prix et des fabricants différents, mais contenir toujours le même ingrédient actif. L\'ingrédient actif lui-même et le dosage sont particulièrement importants pour l\'effet des médicaments dans le corps. Les patients en pharmacie obtiennent souvent un médicament différent de celui prescrit par le médecin sur l\'ordonnance – à condition que les médicaments soient comparables. Il peut y avoir des raisons thérapeutiques et économiques au changement. - Ordonnance scannée - Pour des raisons de sécurité, les ordonnances importées à partir d\'un imprimé papier ne doivent comporter aucune donnée personnelle ou médicale. \n\n Connectez-vous à cette application avec une carte de santé ou une application d\'assurance pour afficher toutes les informations contenues dans l\'ordonnance. + Préparation de remplacement possible + En raison des exigences légales de votre compagnie d\'assurance maladie, une alternative avec le même principe actif peut vous être proposée. \n\n Les médicaments peuvent avoir une apparence et un nom différents, avoir des prix et des fabricants différents, mais contenir toujours le même principe actif. Le principe actif lui-même et le dosage sont déterminants pour l’effet des médicaments sur l’organisme. Les patients reçoivent souvent à la pharmacie un médicament différent de celui prescrit par le médecin, à condition que le médicament soit comparable. Il peut y avoir des raisons thérapeutiques et économiques à ce changement. + Recette numérisée + Les ordonnances importées à partir d\'une copie papier ne peuvent pas afficher d\'informations personnelles ou médicales pour des raisons de sécurité. \n\n Connectez-vous à cette application avec votre carte de santé ou votre application d\'assurance pour afficher toutes les informations contenues dans l\'ordonnance. Recette incorrecte - Cette ordonnance a été émise de manière incorrecte. - Ordonnance scannée - frais de service d\'urgence + Cette ordonnance n\'a pas été délivrée correctement. + Frais de service d\'urgence Dosage selon les instructions écrites téléphone - placer - Poster + site web + Mail Tri par distance impossible. D\'ACCORD Entrez le code PIN actuel - Code PIN saisi incorrect - Le NIP actuel de votre carte Santé + Mauvais code PIN saisi + Le code PIN actuel de votre carte Santé carte bloquée - Débloquez votre carte dans Paramètres > Débloquer la carte. + Déverrouillez votre carte dans Paramètres > Déverrouiller la carte. Pour des raisons de sécurité, veuillez saisir votre code PIN actuel. - NIP oublié + Code PIN oublié Recette incorrecte médicament - Quelque chose semble s\'être mal passé lors de la création de votre recette. Signaler une erreur ? + Quelque chose semble s\'être mal passé lors de la création de votre recette. Signaler une erreur ? Rapport Pas connecté Enregistrer par - carte de santé + Carte de santé biométrie Pas connecté - Votre avis nous intéresse. Merci de prendre cinq minutes pour répondre à notre enquête. Merci d\'avance. - avis d\'avertissement + Votre avis nous intéresse. Veuillez prendre cinq minutes pour répondre à notre enquête. Merci beaucoup d\'avance. + Avis d\'avertissement Pharmacie ajoutée aux favoris Pharmacie supprimée des favoris Mes pharmacies Force du mot de passe très bonne - Échec de l\'opération d\'écriture + L\'opération d\'écriture n\'a pas réussi Le code PIN n\'a pas pu être enregistré Rapport Attribuer un code PIN Règle d\'accès violée - Vous n\'êtes pas autorisé à accéder au répertoire de la carte. + Vous n\'êtes pas autorisé à accéder au répertoire de cartes. Attribuez votre propre code PIN - La carte est sécurisée par un code PIN de votre caisse maladie (PIN transport), veuillez attribuer votre propre code PIN. + La carte est sécurisée par un code PIN de votre caisse d\'assurance maladie (PIN de transport).Veuillez saisir votre propre code PIN. Mot de passe introuvable - Il n\'y a pas de mot de passe stocké sur votre carte. + Aucun mot de passe n\'est stocké sur votre carte. vous avez été déconnecté Connectez-vous à nouveau pour mettre à jour vos recettes. - numéro d\'ingrédient actif + Numéro d\'ingrédient actif puissance et unité Utilisé il y a %s minutes - Réclamé le %s - Racheté à l\'instant - Racheté à %s heures + Échangeé le %s + Récupéré tout à l\'heure + Échangeé à %s heures Ordres - Cette prescription a été rachetée pour vous dans le cadre d\'un traitement. - frais de service d\'urgence - Cette ordonnance ne peut être exécutée de nuit dans une pharmacie sans le paiement supplémentaire d\'un forfait de service d\'urgence. + Cette ordonnance a été exécutée dans le cadre d’un traitement pour vous. + Frais de service d\'urgence + Cette ordonnance ne peut être exécutée la nuit en pharmacie sans le paiement supplémentaire des frais de service d’urgence. Cherche ici - Idées - Partager l\'emplacement dans les paramètres. + Paramètres + Partager la position dans Paramètres. Près de moi Maintenez enfoncé pour modifier le nom. - Saisissez le nouveau nom du profil. - Vous devez être connecté pour recevoir des ordonnances numériques de votre cabinet. - Recevoir des recettes numériquement ? - Faites glisser l\'écran vers le bas pour l\'actualiser. - Aucune ordonnance - Ajoutez des recettes à l\'aide du bouton + dans le coin supérieur droit. - Enregistrer - archives d\'ordonnances + Entrez le nouveau nom du profil. + Pour recevoir des ordonnances numériquement de votre cabinet, vous devez être connecté. + Recevoir des ordonnances par voie numérique ? + Déroulez l’écran pour actualiser. + Aucune recette + Ajoutez des recettes en utilisant le bouton + dans le coin supérieur droit. + Registre + Archives de recettes Peut-être plus tard - Enregistrer + Registre Éditer la photo de profil - archives d\'ordonnances + Archives de recettes Entrez le nom - Enregistrer sur ordinateur + Sauvegarder Ma commande - Destinataire : en - recettes + Destinataire : dans + Recettes Pharmacie Envoyer Changement - A retirer à la pharmacie + À récupérer à la pharmacie Livraison par coursier - Livraison par courrier + Livraison par correspondance %s Recettes - Échange impossible - Une ou plusieurs ordonnances n\'ont pas pu être échangées. - Aucune recette sélectionnée - Pour échanger des recettes, au moins une recette doit être sélectionnée. - Ajouter des informations de contact + Impossible de racheter + Une ou plusieurs ordonnances n\'ont pas pu être rachetées. + Aucune recette choisie + Pour utiliser des recettes, au moins une recette doit être sélectionnée. + Ajouter des coordonnées Changement - Sans ordonnance + Pas de recette Vous n\'avez actuellement aucune ordonnance remboursable collection - courrier + livreur Expédition - choisir des recettes + Choisissez des recettes Appuyez ici pour scanner les recettes Appuyez longuement pour modifier les noms - Ajoutez plus de profils, par exemple pour vos enfants ou vos parents - Cliquez sur l\'affichage pour ignorer l\'info-bulle affichée. + Ajoutez des profils supplémentaires, par exemple pour vos enfants ou vos parents + Cliquez sur l\'écran pour ignorer l\'info-bulle qui apparaît. Comment racheter ? - Comment souhaitez-vous recevoir vos médicaments ? + Comment souhaiteriez-vous recevoir vos médicaments ? Échangez directement - Échange de médicaments sur place + Échanger des médicaments sur place Commande Réservez ou faites-vous livrer Prêt - code collectif - codes uniques + Code de collecte + Codes individuels - Vous avez %s ordonnance. + Vous avez %s recette. Vous avez %s recettes. choisissez @@ -743,104 +742,130 @@ Avis Cette application utilise un logiciel de Google pour reconnaître les codes. Apprendre encore plus - À propos du scanner de code de recette - Quelles données le code de recette contient-il ? - Le code recette contient uniquement un identifiant de la recette. Cela permet de retrouver la prescription sur le service de prescription du réseau numérique de santé. Le code de prescription ne contient aucune donnée sur vous ou vos médicaments. - Donc personne ne peut rien faire avec le code de recette seul ? - Correct. Les données de prescription doivent être téléchargées depuis le service de prescription. Cela nécessite une connexion sécurisée. - Qui peut s\'inscrire au service de prescription? - L\'inscription au service de prescription du réseau numérique de santé est possible pour les assurés, les pharmacies, les cabinets médicaux et les hôpitaux. - Pourquoi l\'application e-prescription utilise-t-elle les fonctionnalités de Google ? - Google propose des fonctions qui peuvent être facilement intégrées dans des applications et qui sont constamment développées et mises à jour par Google. Cela garantit que les fonctions fonctionnent sur de nombreux terminaux différents et peuvent être utilisées en toute sécurité. L\'application utilise une fonctionnalité pour améliorer la fonctionnalité de caméra et de numérisation pour les appareils Android (Google ML Kit). - Comment fonctionne l\'amélioration de l\'analyse de Google ML Kit ? - Google ML Kit aide à optimiser l\'image capturée par une caméra afin que les codes de recette puissent être lus même dans de mauvaises conditions d\'éclairage ou avec des modèles de caméra plus anciens. - Les données relatives à la prescription ou à mes médicaments seront-elles transmises à Google ? - Non. Le code de recette lu est enregistré directement dans l\'application. Il ne sera pas transmis à Google. Les données de prescription ne sont pas stockées dans le code, uniquement dans le réseau de santé numérique. De là, ils sont envoyés à l\'application. Google n\'a pas accès au réseau numérique de santé. - Quelles données Google traite-t-il lors de l\'utilisation de ML Kit ? - Google n\'a accès qu\'aux informations techniques sur l\'appareil final utilisé et l\'utilisation générale de la fonction supplémentaire (par exemple, le taux d\'erreur, les paramètres de l\'appareil photo) afin de les enregistrer statistiquement et d\'améliorer ainsi la fonction supplémentaire. Lors de votre accès, Google enregistre temporairement l\'adresse IP de votre terminal. Les informations vous concernant et le contenu de la recette ne seront pas enregistrées par Google. + Informations sur le scanner de code de recette + Quelles données contient le code de recette ? + Le code recette contient uniquement un identifiant pour la recette. Cela signifie que l\'ordonnance est consultable sur le service de prescription du réseau numérique de santé. Le code d\'ordonnance ne contient aucune information sur vous ou vos médicaments. + Donc personne ne peut rien faire avec le code de la recette seul ? + Correct. Les données de prescription doivent être téléchargées depuis le service de prescription. Pour cela, une connexion sécurisée est requise. + Qui peut s\'inscrire au service de prescription ? + L\'inscription au service de prescription dans le réseau numérique de santé est possible pour les assurés, les pharmacies, les cabinets médicaux et les hôpitaux. + Pourquoi l\'application e-prescription utilise-t-elle les fonctionnalités de Google ? + Google propose des fonctions qui peuvent être facilement intégrées dans les applications et que Google développe et met à jour en permanence. Cela garantit que les fonctions fonctionnent sur de nombreux appareils différents et peuvent être utilisées en toute sécurité. L\'application utilise une fonctionnalité pour améliorer la fonctionnalité de caméra et de numérisation pour les appareils Android (Google ML Kit). + Comment fonctionne l\'amélioration de l\'analyse avec Google ML Kit ? + Google ML Kit permet d\'optimiser l\'image capturée par une caméra afin que les codes de recette puissent être lus même dans de mauvaises conditions d\'éclairage ou avec des modèles de caméra plus anciens. + Les données concernant l\'ordonnance ou mes médicaments seront-elles partagées avec Google ? + Non. Le code de recette lu est enregistré directement dans l\'application. Il ne sera pas partagé avec Google. Les données de prescription ne sont pas stockées dans le code, mais uniquement dans le réseau numérique de santé. De là, ils sont transmis à l\'application. Google n\'a pas accès au réseau numérique de santé. + Quelles données Google traite-t-il lors de l\'utilisation de ML Kit ? + Google n\'a accès aux informations techniques sur l\'appareil utilisé et à l\'utilisation générale de la fonction supplémentaire (par exemple taux d\'erreur, paramètres de l\'appareil photo) que pour les enregistrer statistiquement et ainsi améliorer la fonction supplémentaire. Lors de l\'accès, Google enregistre temporairement l\'adresse IP de votre appareil. Les informations vous concernant et le contenu de la recette ne sont pas enregistrées par Google. L\'utilisation de Google ML Kit est-elle volontaire ? - Oui. Cependant, ML Kit est intégré au scanner de code de recette dans la version Android de l\'application e-prescription. Si vous utilisez le scanner de code de recette sur un appareil Android, la fonction ML Kit est également toujours utilisée. Cependant, vous pouvez vous passer du scanner de code de recette. Vos ordonnances peuvent également être chargées dans l\'application si vous vous inscrivez au réseau de santé numérique avec la carte de santé électronique ou via votre application d\'assurance maladie. - Puis-je voir qui a vu mes recettes ? - Oui. Tous les accès à vos données sont entièrement enregistrés dans le réseau de santé numérique. Dans l\'application e-prescription, vous pouvez voir qui a accédé à vos données. - Qui puis-je contacter si j\'ai des questions sur l\'application ou l\'e-prescription ? - Vous trouverez des informations détaillées dans la déclaration de protection des données. - Nombre de boîtes prescrites - Aucune ordonnance - Pour cela, vous avez besoin d\'ordonnances remboursables. - choisir une assurance + Oui. Cependant, ML Kit est intégré au scanner de code de recette dans la version Android de l\'application de prescription électronique. Si vous utilisez le scanner de code de recette sur un appareil Android, la fonction ML Kit est toujours utilisée. Cependant, vous pouvez éviter d\'utiliser le scanner de code de recette. Vos ordonnances peuvent également être chargées dans l\'application si vous vous connectez au réseau numérique de santé avec la carte de santé électronique ou via votre application d\'assurance maladie. + Puis-je voir qui a consulté mes recettes ? + Oui. Tous les accès à vos données sont entièrement enregistrés dans le réseau numérique de santé. Dans l\'application e-prescription, vous pouvez voir qui a accédé à vos données. + Où puis-je contacter si j\'ai des questions sur l\'application ou l\'ordonnance électronique ? + Des informations détaillées peuvent être trouvées dans la déclaration de protection des données. + Nombre de packs prescrits + Aucune recette + Pour cela, vous avez besoin d’ordonnances remboursables. + Choisissez une assurance Rechercher une assurance - Interrompre - Pour quoi souhaitez-vous postuler ? + Annuler + Pour quoi souhaiteriez-vous postuler ? Pour cette application, vous avez besoin d\'une carte et du code PIN associé. - Comment souhaitez-vous contacter votre compagnie d\'assurance ? - Votre compagnie d\'assurance vous propose les possibilités de contact suivantes - Votre compagnie d\'assurance vous propose les possibilités de contact suivantes + Comment souhaitez-vous contacter votre compagnie d’assurance ? + Votre compagnie d\'assurance propose les options de contact suivantes + Votre compagnie d\'assurance propose l\'option de contact suivante Fermer - PIN mal saisi. + Code PIN saisi incorrectement. Numéro d\'accès mal saisi - PUK entré incorrectement. - notes de frais - Afficher les reçus de dépenses - notes de frais - Pour recevoir les notes de frais, vous devez être connecté au serveur. + PUK mal saisi. + reçus de dépenses + Afficher les reçus de coûts + reçus de dépenses + Pour recevoir les reçus de dépenses, vous devez être connecté au serveur. Connecter - Pas de reçus de dépenses + Pas de reçus de frais Désactiver - Interrompre - désactiver la fonction - Cela supprimera tous les reçus de cet appareil et du serveur. - Recevoir les notes de frais - Vos justificatifs de coûts sont également enregistrés sur le serveur de recettes. - Recevoir + Annuler + Désactiver la fonction + Cela supprimera tous les reçus de dépenses de cet appareil et du serveur. + Recevoir des reçus de coûts + Vos justificatifs de frais sont également enregistrés sur le serveur de recettes. + Reçu Total : %s %s Choisir Diviser - Éteindre - Éteindre + Supprimer + Supprimer Soumettre %s € prix total - Conseil : Soumettez les justificatifs de dépenses via l\'application d\'assurance - Soumettez facilement des reçus de frais via l\'application de votre compagnie d\'assurance. À l\'étape suivante, sélectionnez cette application et appuyez sur Partager. + Astuce : Soumettez les justificatifs de frais via l\'application d\'assurance + Soumettez facilement les reçus de coûts via l’application de votre compagnie d’assurance. À l\'étape suivante, sélectionnez cette application et appuyez sur Partager. Pratique Pharmacie Date montre plus Identification du médicament - Émis pour + Délivré pour KVNR : %s - Date de naissance : %s + Né le : %s D\'ACCORD - Comment soumettez-vous les reçus? - Transférez directement sur l\'application de votre compagnie d\'assurance/bureau d\'aide. Pour ce faire, sélectionnez l\'application sur la page suivante. + Comment soumettre les pièces justificatives ? + Transférez directement sur l\'application de votre caisse d\'assurance/prévoyance. Pour ce faire, sélectionnez l\'application sur la page suivante. ou - Enregistrez le fichier et importez-le ultérieurement dans le portail assurance/aide. + Enregistrez le fichier et importez-le plus tard dans le portail assurances/prestations. Article : %s - Numéro : %s - TVA : %s %% - Prix brut en EUR : %s + Nombre : %s + TVA : %s %% + Prix ​​brut en EUR : %s Frais supplémentaires - frais de service d\'urgence + Frais de service d\'urgence Frais BTM - Frais d\'ordonnance T - les frais d\'approvisionnement + Frais de prescription T + Coûts d\'approvisionnement Service de messagerie Total en EUR : %s prélèvement - Vraiment supprimer ? + Vraiment supprimer ? Le fichier sera supprimé de votre appareil et du serveur. - Éteindre - Posté + Supprimer + Publié Code postal Emplacement - Veuillez entrer votre code postal pour nous contacter. - Veuillez indiquer votre lieu de résidence lorsque vous nous contactez. + Veuillez fournir votre code postal pour nous contacter. + Merci d\'indiquer votre lieu de résidence pour nous contacter. Sera racheté pour vous A été racheté pour vous Vous devez être connecté pour utiliser ce service. - application d\'assurance - carte de santé + Application d\'assurance + Carte de santé Code PIN associé requis + Ne peut être utilisé que demain en tant qu\'auto-paieur + Il ne reste que %s jours à utiliser en tant que payeur autonome + \nToujours échangeable en tant qu\'auto-paiement pendant %s jours\n + Valable pendant %s jours seulement + \nValable pour %s jours restants\n + Uniquement valable demain + Des frais s\'appliquent + Prend une assurance + La ou les recettes ont été transférées avec succès. + La recette ne peut pas être traitée. Veuillez réessayer. Vous devrez peut-être choisir une autre pharmacie. + La recette ne peut pas être traitée. La pharmacie signale une erreur inconnue. Si nécessaire, essayez une autre pharmacie. + L\'ordonnance a été rejetée par la pharmacie. L’ordonnance peut être invalide ou votre adresse de livraison ou vos coordonnées peuvent être invalides. + Impossible d\'échanger, veuillez vérifier votre connexion Internet. + La recette a été transférée avec succès. La pharmacie signale cependant une erreur de traitement. Veuillez contacter la pharmacie. + L\'ordonnance a été rejetée par la pharmacie. L\'ordonnance a déjà été utilisée. + L\'ordonnance a été rejetée par la pharmacie. La recette a été supprimée. + La recette n\'a pas pu être transférée. S\'il vous plaît, vérifiez votre connexion à internet et réessayez. + Une ou plusieurs recettes n\'ont pas pu être transférées. + Erreur d\'envoi + Expédié avec succès ! + Erreur à la pharmacie + Erreur à la pharmacie + Contacter la pharmacie + Ordonnance déjà utilisée + Recette supprimée + Pas d\'Internet Pour recevoir les journaux d\'accès, vous devez être connecté au serveur. Vous pouvez toujours exécuter l’ordonnance en pharmacie pendant cette période, mais vous devrez payer vous-même la totalité du prix d’achat du médicament. Vous pouvez également demander à votre cabinet de faire rééditer l’ordonnance. Prêt @@ -849,4 +874,13 @@ Dans l\'application Faites scanner ce code dans votre pharmacie. Demande de correction de facturation + médicament + Veuillez saisir au moins 1 caractère. + Ou. Essayez l\'application en mode démo + Le mode de démonstration + Le mode de démonstration + Utiliser le mode démo + Mode démo activé + Terminez ici + Activer le mode démo diff --git a/app/features/src/main/res/values-he-rIL/strings.xml b/app/features/src/main/res/values-he-rIL/strings.xml new file mode 100644 index 00000000..115ccaaf --- /dev/null +++ b/app/features/src/main/res/values-he-rIL/strings.xml @@ -0,0 +1,902 @@ + + + בסדר + לְבַטֵל + חזור + סְבִיב + דִיגִיטָלי. מָהִיר. לבטח. + מזהה משימה + קוד גישה + תנאי שימוש + הגנת מידע + מתכונים + הגישה למצלמה נדחתה + כדי להשתמש בסורק, עליך לאפשר לאפליקציה לגשת למצלמה שלך בהגדרות המערכת. + מקד את המצלמה על קוד מתכון + זה לא קוד מרשם תקף + קוד מרשם זה כבר נסרק + + מתכון %s זוהה + + + זוהו מתכונים %s + + לְבַטֵל + אור מצלמה + לבטל את הסריקה? + בסדר + אל תבטל + בוא נלך + מה אתה צריך: + הזן מספר גישה לכרטיס + הזן את קוד ה-PIN + נסה שוב + החיבור לשרת נכשל. + + יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם. + + + יש לך %s ניסיונות נוספים לפני חסימת הכרטיס שלך. + + אתה יכול למצוא את מספר הגישה בפינה השמאלית העליונה של כרטיס הבריאות שלך. + לְבַטֵל + חפש לפי מפה... + החזק את כרטיס הבריאות כנגד גב המכשיר שלך. + עדיין מחפש … + הזז לאט את הכרטיס בגב המכשיר. + עֵצָה + מקרים של מכשירים עשויים להקשות על החיבור באמצעות NFC. + כרטיס מזוהה + השתדלו לא להזיז את תעודת הבריאות. + נמצא כרטיס בריאות. בבקשה אל תזוז. + החיבור אבד + הצמד שוב את כרטיס הבריאות שלך לגב המכשיר + גרסה: %s + בניית hash: %s + תפריט ניפוי באגים + פתוח עד %s + פָּתוּחַ כָּל הַיוֹם + חוֹתָם + עוֹרֵך + gematik GmbH\n פרידריכשטראסה 136\n 10117 ברלין + מנכ\"ל: ד\"ר. med. מרקוס לייק דיקן\n בית משפט לרישום: בית המשפט המחוזי ברלין-שרלוטנבורג\n מספר רישום מסחרי: HRB 96351\n מספר זיהוי מע\"מ: DE241843684 + אחראי על התוכן + ד\"ר. med. מרקוס לייק דיקן + איש קשר + הודעה + אנו שואפים להשתמש בשפה שוויונית בין המינים. אם אתה מבחין בשגיאות, נשמח לשמוע ממך באימייל. + הפלטפורמה המודרנית של גרמניה לרפואה דיגיטלית + כתוב מייל + פתח אתר + ברוך הבא + התחל את הרישום + לבטל נעילה + הירשם + לְבַטֵל + בִּטָחוֹן + משפטי + חוֹתָם + הגנת מידע + תנאי שימוש + פרטים + סמן כמי שנפדה + סמן כלא מומש + צורת מינון + גודל אריזה + מבוטח + שֵׁם מִשׁפָּחָה + כתובת + תאריך לידה + ביטוח בריאות/משלם + סטָטוּס + מספר ביטוח + רושם + שֵׁם מִשׁפָּחָה + רופא מומחה + מספר רופא (LANR) + מוֹסָד + שֵׁם מִשׁפָּחָה + כתובת + מספר צמח + מספר טלפון + כתובת דוא\"ל + תאונת עבודה + יום התאונה + מספר חברה או מעסיק בתאונות + האם תרצה למחוק את המתכון הזה לצמיתות? + לִמְחוֹק + לְבַטֵל + שעות פתיחה + אתר אינטרנט + ניתן לפדות רק היום בתור משלם עצמי + הירשם + הפעל NFC + אנא הפעל את פונקציית ה-NFC של המכשיר שלך כדי להיכנס עם כרטיס הבריאות שלך. + לְהַפְעִיל + נכון + מימוש מרשמים? + האם ברצונך לסמן את המרשמים כממומשים? + לא נפדה + נפדה + נפתח בשעה %s + +49 800 277 377 7 + מוקד טכני + פתח סורק למרשמים + הגדרות + הדחק צילומי מסך + מונע הצגת תמונת תצוגה מקדימה בעת החלפת יישומים + האם אתה מאפשר למרשם אלקטרוני לנתח את התנהגות השימוש שלך באופן אנונימי? + מידע טכני + אבטחת נתוני המרשם שלך + אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. + השליחה נכשלה + לא הוגדרה תוכנת דואר אלקטרוני + אין תוצאות + לא הצלחנו למצוא תוצאות עבור מונח חיפוש זה. + רישיונות קוד פתוח + איש קשר + התקשר למוקד הטכני + קחו חלק בסקר + +49 800 277 377 7 + אני רוצה לעזור לשפר את האפליקציה הזו + זה כולל מידע על חומרה ותוכנה על הטלפון שלך, הגדרות של אפליקציית המרשם האלקטרוני והיקף השימוש, אך לעולם לא נתונים אודותיך או בריאותך. + הנתונים יהיו זמינים ל-gematik GmbH רק על ידי הגורם לעיבוד הנתונים ויימחקו לאחר 180 יום לכל המאוחר. אתה יכול לבטל את הניתוח בכל עת בתפריט האפליקציה. + נתונים אלו מאפשרים לנו להבין אילו פונקציות נמצאות בשימוש תכוף ולשפר אותן. אנחנו יכולים גם להעריך כמה זמן צריך לתמוך בטכנולוגיה ישנה יותר ומתי אנחנו יכולים, למשל, להפוך גירסת מערכת הפעלה חדשה יותר לחובה מבלי להשפיע על (יותר מדי) משתמשים. + שפר את האפליקציה + ניתוח אנונימי נשאר מושבת + %s תודה על תמיכתך! + הירשם + אנא הזדהו כדי להוריד מתכונים. + הערה לבתי מרקחת: אנו מקבלים את פרטי ההתקשרות והמידע על בתי מרקחת מאת mein-apothekenportal.de של איגוד בתי המרקחת הגרמני האם גילית שגיאה או שברצונך לתקן נתונים? + למד עוד + בתי מרקחת + למרבה הצער זה לא עבד \uD83D\uDE15 + אנא נסה זאת שוב. + הזן את הסיסמה + נוסף + נְגִישׁוּת + תקריב + מאפשר לך להגדיל את האפליקציה על ידי צביטה לזום. + סיסמה + אבטח את הנתונים שלך עם סיסמה לבחירתך. + סיסמה + להציל + הראה סיסמה + חזור על הסיסמה + המלצות: %s + כתוב מייל + כאשר אתה שולח את ההודעה שלך, המידע הבא על החומרה ומערכת ההפעלה המשמשת מועבר: + מימוש באתר בלבד + אתה עדיין לא יכול לשלוח מרשמים אלקטרוניים לבית המרקחת הזה. + כרגע פתוח + שירות מסנג\'ר + מִשׁלוֹחַ + לְסַנֵן + לְסַנֵן + אין מיקום זמין + מובן + התאמות חוזרות ונשנות של סיסמא + שגיאה 20 10 76631 + תעודת כרטיס הבריאות שלך לא תקפה. אולי פג תוקף הכרטיס שלך? נא לפנות לקופת החולים שלך. + ניסיונות התחברות לא מוצלחים + + זוהו %s ניסיונות התחברות לא מוצלחים. + + + זוהו %s ניסיונות התחברות לא מוצלחים. + + בחר את הגיבוי הטוב ביותר למכשיר + זו יכולה להיות טביעת אצבע, תבנית החלקה או משהו דומה + אסימונים + אסימוני גישה + אסימוני SSO + אין אסימון גישה זמין + אין אסימון SSO זמין + הועתק ללוח + לחץ כדי להעתיק את האסימון ללוח + תקף רק היום + להתיר + אין חיבור לשרת + אנא נסה שוב בעוד מספר דקות + טען שוב + הצג אסימונים + איך תרצו לאבטח את האפליקציה? + הודעה + לא הוגדר גיבוי מכשיר עבור מכשיר זה + אנו ממליצים לך להגן בנוסף על המידע הרפואי שלך באמצעות אבטחת מכשירים כגון קוד או ביומטריה. + אל תציג הודעה זו שוב בעתיד. + חיבור נכשל. לא ניתן היה ליצור חיבור לרשת. + התקשורת עם השרת נכשלה: קוד מצב %s . + התקשורת עם השרת נכשלה: אנא בדוק את חיבור האינטרנט ואת הגדרות השעה/תאריך. + אַזהָרָה + ייתכן שלמכשיר שלך יש אבטחה מופחתת + זה יכול להיגרם, למשל, על ידי התקנים שעברו מניפולציות או כאשר מצב מפתח מופעל. אנו ממליצים לא להשתמש באפליקציה במכשירים שבורים מטעמי אבטחה. + אני מכיר בסיכון המוגבר ועדיין ארצה להמשיך. + מדוע מכשירים עם גישת שורש הם סיכון אבטחה פוטנציאלי? + למד עוד + https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html + שם הפרופיל + נא להזין שם לפרופיל החדש. + שם פרופיל + פרופילים + כיצד לזהות כרטיס בריאות התומך ב-NFC + אין אפשרות ליצור קשר דרך האפליקציה הזו + אנא השתמש בערוצים הרגילים כדי ליצור קשר עם חברת הביטוח שלך. + כרטיס בריאות וקוד PIN + PIN בלבד + רישום באפליקציית המרשם האלקטרוני + שדה השם לא יכול להיות ריק. + פרופיל עם השם שהזנת כבר קיים. + פּרוֹפִיל + נבחרו %s + צבע רקע + אפור אביב + טל שמש + זה! האם! וָרוֹד! + עֵץ + ירח כחול ספטמבר + לא מחובר + קשורים ביחד + מחובר לאחרונה ב- %s + מחק פרופיל? + פעולה זו תמחק את כל הנתונים מהפרופיל במכשיר זה. המרשמים שלך ברשת הבריאות יישמרו. + לִמְחוֹק + לְבַטֵל + מחק פרופיל + אתה רוצה למחוק את הפרופיל האחרון. + האפליקציה דורשת לפחות פרופיל אחד. נא להזין שם לפרופיל החדש. + שגיאה 20 10 76831 + לא ניתן היה להגיע אל ספריית כרטיסי הבריאות. בבקשה נסה שוב. + תוכל למצוא מידע מאומת במומחיות על מחלות, קודי ICD ונושאי מניעה וטיפול בפורטל הבריאות הלאומי. + פתח את health.bund.de + שינינו את מדיניות הפרטיות + אפליקציית המרשם האלקטרוני התפתחה. זה הצריך לעדכן את מדיניות הפרטיות שלנו. + פתח את מדיניות הפרטיות + זה השתנה מאז %s : + מה קורה כשפותחים את האפליקציה? + מה קורה אם אני משתמש בפונקציית המצלמה/קורא מתכונים עם המצלמה? + אין מתכונים חדשים זמינים + + המתכון החדש %s + + + %s מתכונים חדשים + + פָּדִי + בגאולה + נפדה + לא ידוע + הצג יומני גישה + מי ניגש למתכונים שלך ומתי? + מפתח גישה לשירות המרשם + יומני גישה + אין יומני גישה + אין עדיין יומני גישה. + המתכון נמצא כעת בתהליך ולא ניתן למחוק אותו + לְקַבֵּל + כנראה שזה לא עבד + אנו מודעים לכך שלקשר עם כרטיס הבריאות יש מלכודות. בעתיד, הרישום אמור להיות אפשרי גם באמצעות אפליקציית ביטוח בריאות מאומתת. \n\n כמו כן, אנו פועלים להבטיח שניתן יהיה לממש מרשמים באופן דיגיטלי ללא הרשמה. \n\n שמתם לב למשהו במהלך התהליך הזה שהייתם רוצים לחלוק איתנו? אנא כתבו לנו, נשמח גם לקבל משוב ביקורתי מאוד. + טיפים לחיבור + שפר את חוזק החיבור + במידת הצורך, הסר את כיסוי המגן. + אם המכשיר רוטט ואז החיבור מתנתק, חפש את המיקום האופטימלי ברדיוס קטן. + העבר את המכשיר לאט מאוד על פני המפה. + הנח את המכשיר ישירות על הכרטיס. + לשם כך, הנח את כרטיס הבריאות על משטח שטוח (למשל שולחן). + שפר את חוזק החיבור + שימו לב למיקום חיישן ה-NFC + גלה היכן נמצא חיישן NFC במכשיר שלך (כאן, למשל, סקירה כללית של מכשירים מ- %s ). + במקרים מסוימים, המיקום של חיישן NFC יכול להיות שונה בתוך סדרת דגמים (כאן, למשל, המידע עבור %s ). + העצה הבאה + נוסף + סגור + לְנַסוֹת + כתוב לנו + חיפוש בית מרקחת ברישיון + לִפְדוֹת + מתכון סרוק + נסרק ב- %s + סומן כמומש ב- %s + איך אתה רוצה להמשיך? + להזמין + זמין בקרוב + הזמן עכשיו לאיסוף או שלח אותו באמצעות שליח או משלוח + שמור להזמנה מאוחרת יותר + שמור מתכונים במכשיר + + המשך עם מתכון %s + + + המשך עם %s מתכונים + + חיבור כרטיס בריאות נכשל + הפרופיל הנוכחי כבר מקושר לכרטיס בריאות אחר (מספר קופת חולים %s ). + כרטיס הבריאות שלך כבר מקושר לפרופיל אחר. עבור לפרופיל %s . + להציל + פרטי התקשרות וכתובת + איש קשר + מספר טלפון + אנא ספק מספר טלפון ליצירת קשר. + כתובת אימייל (אופציונלי) + כתובת למשלוח + שם פרטי ושם משפחה + אנא ספק שם פרטי ושם משפחה כדי ליצור איתנו קשר. + מספר רחוב ובית + אנא ספק רחוב ומספר בית כדי ליצור איתנו קשר. + כתובת נוספת (אופציונלי) + הוראות משלוח (לא חובה) + נדרשים פרטים נוספים ליצירת קשר + בטל שינויים? + להשליך + לצורך החיפוש, ספריית בית המרקחת משתמשת בקואורדינטות גיאוגרפיות שנקבעו בעזרת OpenStreetMap. אנו מודים לפרויקט על העזרה הזו. + © OpenStreetMap ( %s ) + https://www.openstreetmap.org/copyright + הגנת מידע ושימוש + נוסף + קיבלת את ה-PIN שלך במכתב מחברת ביטוח הבריאות שלך. + לא התקבל PIN + קוד סודי + בדוק את חיבור האינטרנט של המכשיר ואת הגדרות השעה/תאריך. + כדי להיכנס, לחץ על \"בטל נעילה\". + ננעל בחוץ? אנא אמת את האישורים הביומטריים שלך במכשיר זה. + שכחת את הסיסמא? נא למחוק את האפליקציה ולאחר מכן להתקין אותה מחדש. אתה יכול לגלות מדוע ב- %s שלנו. + אזור עזרה + גודל אריזה ויחידה + רכיב פעיל + כמות החומר הפעיל + שם אצווה + Exp + קטגוריה + תַרכִּיב + לְקַבֵּל + בטל + הודעה + עזרו לנו לשפר את האפליקציה הזו + הזן את הסיסמה + הסיסמה חייבת להיות באורך שמונה תווים לפחות + חוזק הסיסמה אינו מספיק + חוזק הסיסמה מספיק + הסיסמה גלויה + הסיסמה אינה גלויה + ביומטריה + סיסמה + ממתין לתשובה + אין מתכונים + אין לך כרגע מרשמים שניתנים למימוש. + לעדכן + יציאה אוטומטית + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. התחבר מחדש כדי לקבל מתכונים עדכניים. + לְחַבֵּר + קיבלתם תדפיס נייר? + הוסף מתכונים לרשימה שלך על ידי הקשה על כפתור הסריקה בפינה השמאלית העליונה. + סרוק תדפיס נייר + כדי לקבל מרשמים אוטומטית, עליך להיות מחובר. + הירשם + ללא מרשמים מומשים + המרשמים המומשים שלך מוצגים כאן. מטעמי הגנה על נתונים, המתכונים שלך יימחקו משרת המתכונים לאחר 100 ימים. + ללא מרשמים מומשים + המרשמים המומשים שלך מוצגים כאן. הוסף מתכונים באמצעות סריקה כדי להתחיל לממש. + ניהול מכשירים + מכשירים מחוברים + רשום מאז %s (מכשיר זה) + רשום מאז %s + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. + קוד סודי + הזן PIN (כרטיס בריאות). + נוסף + הירשם + מכשירים מחוברים + הסר מכשיר? + לְבַטֵל + לְהַסִיר + להסיר את המכשיר הזה? + האם ברצונך להסיר %s ? + אם תסיר %s , החיבור לשרת המתכונים ינותק לצמיתות תוך 12 שעות לכל המאוחר. + המכשירים נטענים... + אין מכשירים + אין מכשירים מחוברים לכרטיס בריאות זה. + נסה שוב + או - או :-( + לא ניתן היה לטעון את רשימת ההתקנים. + wwweg… + אין חיבור אינטרנט. + תרופות וחבישות + סמים + חלוקת תרופות מרשם בהתאם לסעיף 4 AMVV + אתה צריך עזרה? + ריכזנו עבורכם כמה טיפים לפתרון הבעיות הנפוצות ביותר. + התחל טיפים לחיבור + לבטל נעילה + כרטיס חסום + ה-PIN הוזן שגוי שלוש פעמים. לכן הכרטיס שלך נחסם מטעמי אבטחה. + בטל את נעילת הכרטיס + הזן PUK + עם ה-PIN שלך קיבלת מחברת הביטוח שלך קוד PUK בן 8 ספרות. + בחר PIN חדש + אתה יכול לבחור את מספר הזיהוי האישי החדש שלך (PIN) בעצמך (6 עד 8 ספרות). + זכרתם את ה-PIN שלכם? + אנא רשום את ה-PIN שלך ושמור אותו במקום בטוח. + לְבַטֵל + בסדר + פתיחה לא אפשרית + הגעת למספר המרבי של ביטול נעילה של כרטיס עם PUK זה או שהזנת אותו שוב ושוב באופן שגוי. אנא פנה לחברת הביטוח שלך. + אתה יכול להשתמש ב-PUK אחד עבור עד 10 ביטולי נעילה. + הכרטיס לא נעול + מה אתה צריך: + כרטיס הבריאות שלך + PUK של כרטיס הבריאות שלך + נוסף + כרטיס בריאות + הזמנת PIN או כרטיס + הירשם + איך אתה רוצה להיכנס? + כרטיס בריאות מאופשר NFC + PIN עבור כרטיס הבריאות + אין לך עדיין כרטיס בריאות ו-PIN התומכים ב-NFC? + הגש בקשה עכשיו + או: היכנס עם %s . + אפליקציית ביטוח הבריאות שלך + "תוכל למצוא את מספר הגישה שלך בפינה השמאלית העליונה של כרטיס הבריאות שלך." + לכרטיס שלי אין מספר גישה + + יש לך %s עוד ניסיון אחד לפני שהכרטיס שלך ייחסם. + + + יש לך %s ניסיונות נוספים לפני חסימת הכרטיס שלך. + + הנח את כרטיס הבריאות בגב הטלפון + התהליך הבא עשוי להימשך עד 30 שניות. + הנח את הכרטיס %s בגב הטלפון. + באזור הימני העליון + באמצע באזור העליון + באזור השמאלי העליון + באזור האמצעי מימין + אֶמצַע + באזור האמצעי משמאל + באזור הימני התחתון + באמצע באזור התחתון + באזור השמאלי התחתון + עֶזרָה + נשלח לפני %s דקות + נשלח ב- %s + נשלח רק עכשיו + נשלח בזמן %s + לא תקף יותר + הרשמה באפליקציה + בחר ביטוח + לא מצאת את מה שחיפשת? רשימה זו מתרחבת כל הזמן. הרישום בכרטיס בריאות כבר נתמך על ידי כל קופת חולים. + משוב מאפליקציית המרשם האלקטרוני + אנו מצפים למשוב שלך. אנא השתמש במקום הבא ודייק ככל האפשר: + PUK + סגור + חבל… + למרבה הצער, המכשיר שלך אינו עומד בדרישות המינימום לרישום באפליקציית המרשם האלקטרוני. לצורך אימות מאובטח עם כרטיס הבריאות שלך, נדרשים לפחות אנדרואיד 7 ושבב NFC. + למד עוד + לשמור נתוני כניסה? + להציל + אל תחסוך + הודעה + מטעמי אבטחה, החיבור לשרת המתכונים מתנתק לאחר 12 שעות. כדי להתחבר מחדש, תזדקק לכרטיס בריאות ו-PIN עבור כל תהליך חיבור. + הגדר אבטחה ביומטרית + לא ניתן לשמור נתוני גישה. לפני כן, הגדר אבטחה ביומטרית (למשל טביעת אצבע) במכשיר שלך. + לְבַטֵל + הגדרות + הודעה + לְקַבֵּל + אבטחת נתוני המרשם שלך + \"אפליקציה זו משתמשת בחיישן הביומטרי המאובטח ביותר שמספק המכשיר שלכם כדי לאבטח את האישורים שלכם באזור מוגן של אחסון המכשיר.\" + האבטחה הביומטרית של נתוני הגישה שלך מאפשרת לך לפתוח את האפליקציה הזו בעתיד, להציג, לאחזר, לממש או למחוק מרשמים ללא כרטיס בריאות והקלדת ה-PIN שלך. + אנא ודא שלאנשים שאיתם אתה עשוי לחלוק מכשיר זה ואשר המאפיינים הביומטריים שלהם עשויים להיות מאוחסנים במכשיר זה, יש גם גישה למרשמים שלך. + שלצערי לא עבד + האימות עם אפליקציית ביטוח הבריאות לא הצליח. + פג תוקף ב- %s + המתכון כבר נמחק מהשרת + אנא תקן את הערך שלך או בטל שינויים + נכון + נתוני מבוטח + שֵׁם מִשׁפָּחָה + ביטוח + מספר ביטוח + מספר גישה לכרטיס + הירשם + להתנתק + להציל + שינוי + ערוך תמונת פרופיל + נוסף + השרת לא מגיב + בבקשה נסה שוב מאוחר יותר. + נסה שוב + חפש ביטוח + להתחבר לשרת המתכונים עכשיו? + התחברת בהצלחה + החיבור אבד + להתחבר לשרת המתכונים עכשיו? + אין אסימונים + תקבל אסימון כאשר תיכנס לשירות המרשם.\n + הזמנות + בחר את ה-PIN הרצוי + בטל את נעילת הכרטיס + בחר PIN + חזור על PIN + הערכים שונים זה מזה. + אין פקודות + עדיין אין לך הזמנות. + זֶה עַתָה + בשעה %s + עגלת הקניות מוכנה + המתכון התווסף לעגלת הקניות שלך. נא להיכנס לאתר בית המרקחת להשלמת ההזמנה. + פתיחת עגלת קניות + הצג את קוד האיסוף הזה בבית המרקחת. + קוד איסוף התקבל + אין אפשרות להציג את ההודעה + אנא צור קשר עם בית המרקחת שלך ( %s ). + הצג קישור לעגלת קניות + הצג קוד איסוף + הצג את ההודעה + %s בשעה %s + המתכון נשלח אל %s . + סקירת הזמנה + חָדָשׁ + קוּרס + הסדר + ללא תשלום עבור המתקשר. זמני השירות: שני - שישי 8:00 בבוקר - 20:00 למעט בחגים לאומיים + בֵּית מִרקַחַת + בחר את ה-PIN הרצוי + ה-PIN הרצוי נשמר + כרגע פתוח וקרוב אליי + סנן לפי… + להתחיל בחיפוש + משימה ישירה + בתי מרקחת + מספר טלפון (אופציונלי) + חפש לפי שם או כתובת + אין מידע חוקי של בית מרקחת + לא נמצא מידע עדכני על בית מרקחת זה. הערך של בית מרקחת זה יימחק. + בסדר + ספריית בתי המרקחת אינה זמינה + נכון לעכשיו, לא ניתן לגשת למידע עדכני על בית מרקחת זה. אנא בדוק את חיבור האינטרנט שלך. + לְבַטֵל + נסה שוב + שמור סביבה + לא ניתן להיכנס + נראה שמאפייני הכניסה הביומטריים שלך השתנו. אנא היכנס שוב עם כרטיס הבריאות שלך. + לְבַטֵל + הירשם + פרופיל 1 + קרוב אליי + ניתן למימוש מאוחר יותר + ניתן למימוש מ %s + שיפורים במוצר + ניתוח אנונימי + עזרו לנו לשפר את האפליקציה הזו. כל נתוני השימוש נאספים בעילום שם ומשמשים אך ורק לשיפור חווית המשתמש. + אבטחת מכשיר + הגדרות אישיות + נְגִישׁוּת + שיפורים במוצר + נוסף מתכון + המתכון כבר זמין + אירעה שגיאה במהלך הייבוא + לִמְחוֹק + מתכון סרוק + הכנה להחלפה אפשרית + נשכח PIN + + %s מתכון + + + %s מתכונים + + קראתי ומסכים למדיניות הפרטיות ותנאי השימוש. + הגנת מידע + תנאי שימוש + אנחנו נשמח: + שפר את השימושיות. + זיהוי שגיאות וקריסות. + כל הנתונים נאספים כמובן בעילום שם. + אתה יכול לשנות החלטה זו בכל עת בהגדרות המערכת. + לְהַמשִׁיך + לְקַבֵּל + אפליקציה זו משתמשת בשיטה הבטוחה ביותר שמספקת המכשיר שלך. + להציל + בחר + תְרוּפָה + שם מסחרי + כן + לא + מִנוּן + יום הוצאה לאור + מרשם זה ימומש עבורך במסגרת טיפול. + לא מוגדר + תשלום נוסף + תְרוּפָה + הוראות הגשה + זכאי לפי BVG + הכנה חלופית + שם המתכון + אריזה + הוראות ייצור + תיאור + ניתנו על ידי + הונפק ב: + רכיב פעיל + רשום + לְקַבֵּל + מהי משימה ישירה? + עם הפניה ישירה, מרשם מרופא או בית חולים ממולא ישירות בבית מרקחת. מבוטחים אינם חייבים לבצע כל פעולה ואינם יכולים להתערב בתהליך הפדיון. \n\n הפניות ישירות מופיעות באפליקציית המרשם האלקטרוני כדי להפוך את הטיפול שלך לשקוף יותר עבורך. + עמלת שירות חירום + לפעמים יש צורך בחיפזון. ניתן למלא מרשמים מסוימים ללא תשלום נוסף של דמי שירות חירום, למשל בלילה או בחגים. + תרופות בעלות השתתפות עצמית + פטור מתשלום נוסף + בעלי ביטוח בריאות סטטוטורי חייבים לשלם תשלום נוסף של עד עשרה אירו עבור תרופות מרשם. \n\n גובה התשלום הנוסף תלוי במחיר התרופה שלך. אתה צריך לשלם עבור תרופות שעולות פחות מ-5 אירו בעצמך.\n עבור תרופות יקרות יותר, יש לשלם עשרה אחוזים מהמחיר, אך לפחות 5 אירו ומקסימום 10 אירו. \n\n ילדים וצעירים מתחת לגיל 18 פטורים בדרך כלל מתשלום נוסף. \n\n אם העלויות השנתיות שלך עבור תרופות חורגות ממגבלת הנטל הכספי שלך, תוכל לקבל פטור מהתשלום. שוחח על כך עם קופת החולים שלך. + אתה פטור מתשלום השתתפות עצמית עבור תרופה זו. קופת החולים שלך תכסה את עלות התרופה. + לכמה זמן מרשם זה תקף? + במהלך תקופה זו תוכל לממש את המרשם שלך בכל בית מרקחת בתשלום נוסף של 10 אירו. + הכנה להחלפה אפשרית + עקב דרישות משפטיות מחברת ביטוח הבריאות שלך, ייתכן שתינתן לך חלופה עם אותו חומר פעיל. \n\n תרופות יכולות להיראות ולהיקרא שונות, בעלות מחירים ויצרנים שונים, אך עדיין מכילות את אותו חומר פעיל. לחומר הפעיל עצמו ולמינון יש חשיבות מכרעת להשפעת התרופות בגוף. לעתים קרובות חולים מקבלים בבית המרקחת תרופה שונה מזו שרשם הרופא - בתנאי שהתרופה דומה. ייתכנו סיבות טיפוליות וכלכליות לשינוי. + מתכון סרוק + מרשמים המיובאים מעותק קשיח אינם יכולים להציג מידע אישי או רפואי מסיבות אבטחה. \n\n היכנס לאפליקציה זו באמצעות כרטיס בריאות או אפליקציית ביטוח כדי לראות את כל המידע הכלול במרשם. + מתכון שגוי + מרשם זה הוצא בצורה שגויה. + עמלת שירות חירום + מינון לפי הוראות כתובות + טלפון + אתר אינטרנט + דוֹאַר + מיון לפי מרחק לא אפשרי. + בסדר + הזן PIN נוכחי + הוזן PIN שגוי + ה-PIN הנוכחי של כרטיס הבריאות שלך + כרטיס חסום + בטל את נעילת הכרטיס שלך בהגדרות > ביטול נעילת כרטיס. + מטעמי אבטחה, אנא הזן את ה-PIN הנוכחי שלך. + נשכח PIN + מתכון לא נכון + תְרוּפָה + נראה שמשהו השתבש במהלך יצירת המתכון שלך. לדווח על שגיאה? + להגיש תלונה + לא מחובר + רשום עם + כרטיס בריאות + ביומטריה + לא מחובר + אנו מעוניינים לדעתכם. אנא הקדש חמש דקות למילוי הסקר שלנו. תודה רבה מראש. + הערת אזהרה + בית מרקחת נוסף למועדפים + בית המרקחת הוסר מהמועדפים + בתי המרקחת שלי + חוזק הסיסמא טוב מאוד + פעולת הכתיבה לא הצליחה + לא ניתן היה לשמור את ה-PIN + להגיש תלונה + הקצה PIN + כלל הגישה הופר + אין לך הרשאה לגשת לספריית המפות. + הקצה סיכה משלך + הכרטיס מאובטח ב-PIN מקופת החולים שלך (PIN תחבורה) נא להזין PIN משלך. + הסיסמה לא נמצאה + אין סיסמה מאוחסנת בכרטיס שלך. + נותקת מהמערכת + היכנס שוב כדי לעדכן את המתכונים שלך. + מספר החומר הפעיל + עוצמה ואחדות + מומש לפני %s דקות + מומש ב- %s + נפדה רק עכשיו + מומש בשעה %s + הזמנות + מרשם זה מולא כחלק מטיפול עבורך. + עמלת שירות חירום + לא ניתן למלא מרשם זה בבית מרקחת בלילה ללא תשלום נוסף של דמי שירות חירום. + חפש כאן + הגדרות + שתף מיקום בהגדרות. + קרוב אליי + לחץ והחזק כדי לערוך את השם. + הזן את השם החדש עבור הפרופיל. + כדי לקבל מרשמים באופן דיגיטלי מהמרפאה שלך, עליך להיות מחובר. + מקבלים מרשמים בצורה דיגיטלית? + משוך למטה את המסך כדי לרענן. + אין מתכונים + הוסף מתכונים באמצעות כפתור + בפינה השמאלית העליונה. + הירשם + ארכיון מתכונים + אולי אחר כך + הירשם + ערוך תמונת פרופיל + ארכיון מתכונים + הכנס שם + להציל + ההזמנה שלי + נמען: ב + מתכונים + בֵּית מִרקַחַת + לִשְׁלוֹחַ + שינוי + איסוף בבית המרקחת + משלוח באמצעות שליח + משלוח בהזמנה בדואר + %s מתכונים + לא ניתן לפדות + לא ניתן היה לממש מרשם אחד או יותר. + לא נבחר מתכון + כדי לממש מתכונים, יש לבחור לפחות מתכון אחד. + הוסף פרטים ליצירת קשר + שינוי + אין מתכון + אין לך כרגע מרשמים שניתנים למימוש + אוסף + נער שליחויות + מִשׁלוֹחַ + בחר מתכונים + הקש כאן כדי לסרוק מתכונים + לחץ לחיצה ארוכה כדי לערוך שמות + הוסף פרופילים נוספים, למשל עבור הילדים או ההורים שלך + לחץ על התצוגה כדי לדלג על הסבר הכלי שמופיע. + איך לפדות? + כיצד תרצה לקבל את התרופה שלך? + לממש ישירות + מימוש תרופות במקום + להזמין + הזמן או שלח אותו + מוּכָן + קוד איסוף + קודים בודדים + + יש לך מתכון %s . + + + יש לך %s מתכונים. + + לעשות בחירה + כל המתכונים + איזה מתכונים? + נוסף + נוסף + למד עוד + הודעה + אפליקציה זו משתמשת בתוכנה מגוגל כדי לזהות קודים. + למד עוד + מידע על סורק קוד המתכון + אילו נתונים מכיל קוד המתכון? + קוד המתכון מכיל רק מזהה עבור המתכון. המשמעות היא שניתן למצוא את המרשם בשירות המרשם ברשת הבריאות הדיגיטלית. קוד המרשם אינו מכיל מידע אודותיך או אודות התרופה שלך. + אז אף אחד לא יכול לעשות כלום רק עם קוד המתכון? + נכון. יש להוריד את נתוני המרשם משירות המרשם. לשם כך נדרשת התחברות מאובטחת. + מי יכול להירשם לשירות המרשם? + הרשמה לשירות המרשם ברשת הבריאות הדיגיטלית אפשרית למבוטחים, בתי מרקחת, מרפאות ובתי חולים. + מדוע אפליקציית המרשם האלקטרונית משתמשת בתכונות Google? + גוגל מציעה פונקציות שניתן לשלב בקלות באפליקציות ושגוגל מפתחת ומעדכנת ללא הרף. זה מבטיח שהפונקציות פועלות על מכשירים רבים ושונים וניתן להפעיל אותן בבטחה. האפליקציה משתמשת בתכונה כדי לשפר את פונקציונליות המצלמה והסריקה עבור מכשירי אנדרואיד (Google ML Kit). + כיצד פועל שיפור הסריקה עם Google ML Kit? + ערכת Google ML עוזרת לייעל את התמונה שצולמה על ידי מצלמה כך שניתן לקרוא את קודי המתכון גם בתנאי תאורה גרועים או עם דגמי מצלמה ישנים יותר. + האם הנתונים על המרשם או התרופה שלי ישותפו עם Google? + לא. קוד המתכון שנקרא נשמר ישירות באפליקציה. זה לא ישותף עם גוגל. נתוני המרשם אינם נשמרים בקוד, אלא רק ברשת הבריאות הדיגיטלית. משם הם מועברים לאפליקציה. לגוגל אין גישה לרשת הבריאות הדיגיטלית. + אילו נתונים מעבדת Google בעת שימוש ב-ML Kit? + גוגל מקבלת גישה רק למידע טכני על המכשיר בו נעשה שימוש והשימוש הכללי בפונקציה הנוספת (למשל שיעור שגיאות, הגדרות מצלמה) על מנת לתעד זאת באופן סטטיסטי ובכך לשפר את הפונקציה הנוספת. בעת הגישה, Google מתעדת זמנית את כתובת ה-IP של המכשיר שלך. מידע עליך ועל תוכן המתכון אינו מתועד על ידי גוגל. + האם השימוש ב-Google ML Kit הוא התנדבותי? + כן. עם זאת, ML Kit מובנה בסורק קוד המתכון בגרסת האנדרואיד של אפליקציית המרשם האלקטרוני. אם אתה משתמש בסורק קוד המתכון במכשיר אנדרואיד, תמיד נעשה שימוש בפונקציית ML Kit. עם זאת, אתה יכול להימנע משימוש בסורק קוד המתכון. ניתן לטעון את המרשמים שלך לאפליקציה גם אם תיכנס לרשת הבריאות הדיגיטלית עם כרטיס הבריאות האלקטרוני או דרך אפליקציית קופת החולים שלך. + האם אני יכול לראות מי צפה במתכונים שלי? + כן. כל הגישה לנתונים שלך מתועדת במלואה ברשת הבריאות הדיגיטלית. באפליקציית המרשם האלקטרוני תוכל לראות מי ניגש לנתונים שלך. + לאן אוכל לפנות אם יש לי שאלות לגבי האפליקציה או המרשם האלקטרוני? + מידע מפורט ניתן למצוא בהצהרת הגנת המידע. + מספר החבילות שנקבעו + אין מתכונים + בשביל זה אתה צריך מרשמים שניתנים לפדיון. + בחר ביטוח + חפש ביטוח + לְבַטֵל + על מה תרצה להגיש בקשה? + עבור אפליקציה זו אתה צריך כרטיס ואת ה-PIN המשויך. + כיצד תרצה ליצור קשר עם חברת הביטוח שלך? + חברת הביטוח שלך מציעה את אפשרויות ההתקשרות הבאות + חברת הביטוח שלך מציעה את אפשרות ההתקשרות הבאה + סגור + PIN הוזן שגוי. + מספר הגישה הוזן שגוי + PUK הוזן בצורה שגויה. + קבלות עלויות + הצג קבלות עלויות + קבלות עלויות + כדי לקבל קבלות על הוצאות, עליך להיות מחובר לשרת. + לְחַבֵּר + ללא קבלות עלויות + השבת + לְבַטֵל + השבת את הפונקציה + פעולה זו תמחק את כל קבלות ההוצאות מהמכשיר הזה ומהשרת. + קבלת קבלות עלויות + קבלות העלות שלך מאוחסנות גם בשרת המתכונים. + קיבלו + סה\"כ: %s %s + בחר + לְפַצֵל + לִמְחוֹק + לִמְחוֹק + שלח + %s € + מחיר סופי + טיפ: שלח קבלות עלות דרך אפליקציית הביטוח + שלח קבלות עלות בקלות באמצעות האפליקציה של חברת הביטוח שלך. בשלב הבא, בחר את האפליקציה הזו ולחץ על שתף. + תרגול + בֵּית מִרקַחַת + תַאֲרִיך + להראות יותר + מזהה תרופה + הונפק עבור + KVNR: %s + נולד ב: %s + בסדר + איך מגישים מסמכים תומכים? + העבר ישירות לאפליקציה של משרד הביטוח/תגמולים שלך. כדי לעשות זאת, בחר את האפליקציה בעמוד הבא. + אוֹ + שמור את הקובץ וייבא מאוחר יותר לפורטל הביטוח/תגמולים. + מאמר: %s + ספירה: %s + מע\"מ: %s %% + מחיר ברוטו באירו: %s + עמלות נוספות + עמלת שירות חירום + עמלת BTM + דמי מרשם T + עלויות רכש + שירות מסנג\'ר + סך הכל באירו: %s + לִגבּוֹת + באמת למחוק? + הקובץ יימחק מהמכשיר שלך ומהשרת. + לִמְחוֹק + פורסם + מיקוד + מקום + אנא ספק את המיקוד שלך כדי ליצור איתנו קשר. + אנא ציינו את מקום מגוריכם כדי ליצור איתנו קשר. + יגאל עבורך + נגאל עבורך + עליך להיות מחובר כדי להשתמש בשירות זה. + אפליקציית ביטוח + כרטיס בריאות + נדרש PIN משויך + ניתן לממש רק מחר כמשלם עצמי + נותרו רק %s ימים למימוש בתור משלם עצמי + \nעדיין ניתן לפדות כתשלום עצמי למשך %s ימים\n + תקף למשך %s ימים בלבד + \nתקף לעוד %s ימים\n + תקף רק מחר + חיובים חלים + לוקח ביטוח + המתכון/ים הועברו בהצלחה. + לא ניתן לעבד את המתכון. בבקשה נסה שוב. ייתכן שתצטרך לבחור בית מרקחת אחר. + לא ניתן לעבד את המתכון. בית המרקחת מדווח על שגיאה לא ידועה. במידת הצורך, נסה בית מרקחת אחר. + המרשם נדחה על ידי בית המרקחת. ייתכן שהמרשם אינו חוקי או שכתובת המשלוח או פרטי הקשר שלך אינם חוקיים. + לא ניתן לממש, אנא בדוק את חיבור האינטרנט שלך. + המתכון הועבר בהצלחה. עם זאת, בית המרקחת מדווח על טעות בעיבוד. נא לפנות לבית המרקחת. + המרשם נדחה על ידי בית המרקחת. המרשם כבר מומש. + המרשם נדחה על ידי בית המרקחת. המתכון נמחק. + לא ניתן היה להעביר את המתכון. אנא בדוק את חיבור האינטרנט שלך ונסה שוב. + לא ניתן היה להעביר מתכון אחד או יותר. + שגיאה בשליחת + נשלח בהצלחה! + טעות בבית המרקחת + טעות בבית המרקחת + פנה לבית מרקחת + מרשם כבר מומש + המתכון נמחק + אין אינטרנט + כדי לקבל יומני גישה, עליך להיות מחובר לשרת. + אתה עדיין יכול למלא את המרשם בבית מרקחת בתוך תקופה זו, אך תצטרך לשלם את כל מחיר הרכישה עבור התרופה בעצמך. לחלופין, אתה יכול לבקש מהמרפאה שלך להנפיק מחדש את המרשם. + מוּכָן + בקש תיקון + בבית המרקחת + באפליקציה + מסור את הקוד הזה בבית המרקחת שלך. + בקשה לתיקון חיוב + תְרוּפָה + אנא הזן לפחות תו אחד. + אוֹ. נסה את האפליקציה במצב הדגמה + במצב הדגמה + במצב הדגמה + השתמש במצב הדגמה + מצב הדגמה הופעל + סיים כאן + הפעל מצב הדגמה + diff --git a/android/src/main/res/values-it/strings.xml b/app/features/src/main/res/values-it/strings.xml similarity index 53% rename from android/src/main/res/values-it/strings.xml rename to app/features/src/main/res/values-it/strings.xml index f7bf5808..79a83e3b 100644 --- a/android/src/main/res/values-it/strings.xml +++ b/app/features/src/main/res/values-it/strings.xml @@ -1,188 +1,188 @@ OK - Interrompere - Ritorno + Annulla + Indietro in giro Digitale. Veloce. Sicuro. ID attività - codice d\'accesso + Codice d\'accesso Termini di utilizzo Protezione dati - ricette + Ricette Accesso alla telecamera negato - Per utilizzare lo scanner, devi consentire all\'app di accedere alla tua fotocamera nelle impostazioni di sistema. - Metti a fuoco la fotocamera su un codice ricetta + Per utilizzare lo scanner, devi consentire all\'app di accedere alla fotocamera nelle Impostazioni di sistema. + Metti a fuoco la fotocamera su un codice di ricetta Questo non è un codice di prescrizione valido Questo codice di prescrizione è già stato scansionato - %s ricetta riconosciuta + Ricetta %s riconosciuta %s ricette riconosciute - Interrompere - luce della fotocamera + Annulla + Luce della fotocamera Annullare la scansione? OK Non annullare - Eccoci qui + Andiamo Quello che ti serve: - Inserisci il numero di accesso alla carta + Inserisci il numero di accesso della carta inserire il codice PIN - riprova + Riprova Impossibile connettersi al server. - Hai altri %s tentativi prima che la tua carta venga bloccata. - Hai altri %s tentativi prima che la tua carta venga bloccata. + Hai ancora %s tentativo prima che la tua carta venga bloccata. + Hai ancora %s tentativi prima che la tua carta venga bloccata. - Troverai il numero di accesso in alto a destra della tua tessera sanitaria. - Interrompere - Cerca mappa... - Tieni la tessera sanitaria sul retro del tuo dispositivo. + Puoi trovare il numero di accesso in alto a destra sulla tua tessera sanitaria. + Annulla + Cerca per mappa... + Avvicina la tessera sanitaria al retro del tuo dispositivo. Ancora cercando … - Sposta lentamente la scheda sul retro del dispositivo. + Sposta lentamente la carta sul retro del dispositivo. Mancia - Le custodie dei dispositivi possono rendere difficile la connessione tramite NFC. - carta riconosciuta + Le custodie dei dispositivi potrebbero rendere difficile la connessione tramite NFC. + Carta riconosciuta Cerca di non spostare la tessera sanitaria. Tessera sanitaria ritrovata. Per favore, non muoverti. Collegamento perso Avvicina nuovamente la tua tessera sanitaria al retro del dispositivo Versione: %s - Costruisci hash: %s - menu di debug + Crea hash: %s + Menù di debug Aperto fino %s Aperto tutto il giorno impronta editore - Gematik GmbH\n Friedrichstraße 136\n 10117 Berlino - Amministratore delegato: Dott. medico Markus Leyck-Dieken\n Tribunale di registrazione: tribunale distrettuale di Berlino-Charlottenburg\n Numero di registro delle imprese: HRB 96351\n Numero di identificazione IVA: DE241843684 + gematik GmbH\n Friedrichstrasse 136\n 10117 Berlino + Amministratore Delegato: Dott. med. Markus Leyck Dieken\n Tribunale di registrazione: tribunale distrettuale di Berlino-Charlottenburg\n Numero del registro delle imprese: HRB 96351\n Partita IVA: DE241843684 Responsabile del contenuto - dott medico Markus Leyck-Dieken + Dott. med. Markus Leyck Dieken Contatto Avviso - Ci sforziamo di utilizzare un linguaggio neutro rispetto al genere. Se noti errori, non vediamo l\'ora di sentirti via e-mail. + Ci sforziamo di utilizzare un linguaggio equo rispetto al genere. Se noti errori, saremo lieti di risponderti via e-mail. La moderna piattaforma tedesca per la medicina digitale - scrivere posta - sito web aperto + Scrivi e-mail + Apri il sito web Benvenuto Inizia la registrazione - sbloccare + Sbloccare Registrati - Interrompere + Annulla Sicurezza Legale impronta protezione dati Termini di utilizzo - dettagli - Segna come riscattato - Segna come non riscattato - forma di dosaggio - dimensione della confezione + Dettagli + Contrassegna come riscattato + Contrassegna come non redento + Forma di dosaggio + Dimensioni della confezione Persona assicurata Cognome indirizzo Data di nascita - Assicurazione sanitaria / Pagatori + Assicurazione sanitaria/pagatore stato - numero di polizza - Persona che prescrive + Numero di polizza + Prescrittore Cognome - Specialista medico + Medico specialista Numero del medico (LANR) istituzione Cognome indirizzo - Numero locali commerciali - numero di telefono - indirizzo di posta - incidente sul lavoro - giorno dell\'incidente - Numero dell\'azienda o del datore di lavoro dell\'incidente + Numero della pianta + Numero di telefono + Indirizzo e-mail + Infortunio sul lavoro + Giorno dell\'incidente + Numero dell\'azienda o del datore di lavoro Vuoi eliminare definitivamente questa ricetta? - Spegnere - Interrompere + Eliminare + Annulla orari di apertura sito web - Riscattabile solo oggi come autopagante + Riscattabile solo oggi come autopagatore Registrati - Attiva l\'NFC - Attiva la funzione NFC del tuo dispositivo per accedere con la tua tessera sanitaria. + Abilita NFC + Ti invitiamo ad attivare la funzione NFC del tuo dispositivo per accedere con la tua tessera sanitaria. Attivare Corretto Prescrizioni riscattate? - Vuoi contrassegnare le prescrizioni come riscattate? + Desideri contrassegnare le prescrizioni come riscattate? Non riscattato Redento - Apre alle %s - +49 800 277 377 7 + Apre alle %s ora + +49 800 277 3777 Hotline tecnica - Apri lo scanner per le ricette - Idee + Apri lo scanner per le prescrizioni + Impostazioni Elimina gli screenshot - Impedisce la visualizzazione di una miniatura quando si passa da un\'app all\'altra - Consenti a e-recipe di analizzare il tuo comportamento di utilizzo in modo anonimo? + Impedisce la visualizzazione di un\'immagine di anteprima quando si cambia app + Consenti a E-Prescription di analizzare il tuo comportamento di utilizzo in modo anonimo? Informazioni tecniche - Sicurezza dei tuoi dati di prescrizione - Assicurati che anche le persone con cui potresti condividere questo dispositivo e le cui caratteristiche biometriche potrebbero essere memorizzate su questo dispositivo abbiano accesso alle tue prescrizioni. + Sicurezza dei dati della tua prescrizione + Assicurati che anche le persone con cui condividi questo dispositivo e le cui caratteristiche biometriche potrebbero essere memorizzate su questo dispositivo abbiano accesso alle tue prescrizioni. invio fallito - Nessun programma di posta elettronica installato + Nessun programma di posta elettronica configurato Nessun risultato Non siamo riusciti a trovare alcun risultato per questo termine di ricerca. - Licenze Open Source + Licenze open source Contatto Chiama la hotline tecnica Partecipa al sondaggio - +49 800 277 377 7 + +49 800 277 3777 Voglio contribuire a migliorare questa app - Ciò include informazioni su hardware e software sul tuo telefono, impostazioni dell\'app di prescrizione elettronica e portata di utilizzo, ma mai dati su di te o sulla tua salute. - I dati vengono messi a disposizione di gematik GmbH solo dal responsabile del trattamento e vengono cancellati al più tardi dopo 180 giorni. È possibile disattivare nuovamente l\'analisi in qualsiasi momento nel menu dell\'app. - Questi dati ci permettono di capire quali funzioni vengono utilizzate frequentemente e di migliorarle. Inoltre, possiamo stimare per quanto tempo la tecnologia precedente deve essere supportata e quando possiamo, ad esempio, rendere obbligatoria una versione del sistema operativo più recente senza influire su (troppi) utenti. - migliorare l\'app + Ciò include informazioni hardware e software sul tuo telefono, impostazioni dell\'app e-prescription e entità dell\'utilizzo, ma mai dati su di te o sulla tua salute. + I dati verranno messi a disposizione della gematik GmbH solo dal titolare del trattamento e verranno cancellati al massimo dopo 180 giorni. Puoi disattivare l\'analisi in qualsiasi momento nel menu dell\'app. + Questi dati ci permettono di capire quali funzioni vengono utilizzate frequentemente e di migliorarle. Possiamo anche stimare per quanto tempo sarà necessario supportare la tecnologia più vecchia e quando potremo, ad esempio, rendere obbligatoria una versione più recente del sistema operativo senza incidere su (troppi) utenti. + Migliora l\'app L\'analisi anonima rimane disabilitata %s Grazie per il tuo supporto! Registrati - Identificati per scaricare le ricette. - Nota per le farmacie: otteniamo i dettagli di contatto e le informazioni sulle farmacie da mein-apothekenportal.de dell\'Associazione tedesca delle farmacie Hai scoperto un errore o desideri correggere i dati? + Per favore identificati per scaricare le ricette. + Nota per le farmacie: i dati di contatto e le informazioni sulle farmacie li otteniamo da mein-apothekenportal.de dell\'Associazione tedesca dei farmacisti Hai scoperto un errore o desideri correggere i dati? Saperne di più - farmacie - Sfortunatamente, non ha funzionato \uD83D\uDE15 - Si prega di riprovare. + Farmacie + Sfortunatamente non ha funzionato \uD83D\uDE15 + Per favore riprova. Inserire la password Ulteriore Accessibilità Ingrandisci - Consente di ingrandire l\'app pizzicando o allargando le dita (pinch-to-zoom). + Ti consente di ingrandire l\'app pizzicando per ingrandire. parola d\'ordine Proteggi i tuoi dati con una password a tua scelta. parola d\'ordine - Salva sul computer - mostra password + Salva + Mostra password Ripeti la password Raccomandazioni: %s - scrivere posta - Quando invii il tuo messaggio, verranno trasmesse le seguenti informazioni sull\'hardware e sul sistema operativo utilizzato: - Riscatta solo in loco - Non puoi ancora inviare ricette elettroniche a questa farmacia. + Scrivi e-mail + Quando invii il tuo messaggio, vengono trasmesse le seguenti informazioni sull\'hardware e sul sistema operativo utilizzato: + Riscatta solo sul posto + Non è ancora possibile inviare prescrizioni elettroniche a questa farmacia. Attualmente aperto - servizio di messaggeria + Servizio di corriere Spedizione filtro Filtro Nessuna posizione disponibile Inteso - Corrispondenze password ripetute + Corrispondenze ripetute della password Errore 20 10 76631 - Il tuo certificato di tessera sanitaria non è valido. La tua carta è scaduta? Si prega di contattare la propria assicurazione sanitaria. + Il certificato della tua tessera sanitaria non è valido. Forse la tua carta è scaduta? Si prega di contattare la propria compagnia di assicurazione sanitaria. Tentativi di accesso non riusciti Sono stati rilevati %s tentativi di accesso non riusciti. Sono stati rilevati %s tentativi di accesso non riusciti. Scegli il miglior backup del dispositivo - Può trattarsi di un\'impronta digitale, di una sequenza di scorrimento o simili - gettoni - token di accesso + Può trattarsi di un\'impronta digitale, di una sequenza di scorrimento o qualcosa di simile + Gettoni + Token di accesso Token SSO Nessun token di accesso disponibile nessun token SSO disponibile @@ -193,30 +193,30 @@ nessuna connessione al server Riprova tra qualche minuto Carica di nuovo - mostra gettoni - Come vuoi proteggere l\'app? + Mostra gettoni + Come vorresti proteggere l\'app? Avviso - Nessun backup del dispositivo è stato impostato per questo dispositivo - Ti consigliamo di proteggere ulteriormente i tuoi dati medici con la sicurezza del dispositivo come un passcode o dati biometrici. + Per questo dispositivo non è stato configurato alcun backup del dispositivo + Ti consigliamo di proteggere ulteriormente le tue informazioni mediche con la sicurezza del dispositivo come un codice o dati biometrici. Non mostrare più questo avviso in futuro. Connessione fallita. Non è stato possibile stabilire una connessione di rete. Comunicazione con il server non riuscita: codice di stato %s . - Impossibile comunicare con il server: controllare la connessione Internet e le impostazioni di ora/data. + Comunicazione con il server non riuscita: controlla la connessione Internet e le impostazioni di ora/data. avvertimento Il tuo dispositivo potrebbe avere una sicurezza ridotta - Ciò può essere causato, ad esempio, da dispositivi manipolati o da una modalità sviluppatore attivata. Per motivi di sicurezza, non consigliamo di utilizzare l\'app su dispositivi con jailbreak. - Riconosco l\'aumento del rischio e voglio ancora continuare. + Ciò può essere causato, ad esempio, da dispositivi manipolati o dall\'attivazione della modalità sviluppatore. Si consiglia di non utilizzare l\'app su dispositivi jailbroken per motivi di sicurezza. + Riconosco l\'aumento del rischio e vorrei comunque procedere. Perché i dispositivi con accesso root rappresentano un potenziale rischio per la sicurezza? Saperne di più https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html Nome del profilo Inserisci un nome per il nuovo profilo. - nome del profilo - profili + Nome del profilo + Profili Come riconoscere una tessera sanitaria abilitata NFC Nessun contatto possibile tramite questa app - Si prega di utilizzare i consueti canali per contattare la propria compagnia assicurativa. - Tessera Sanitaria e PIN + Ti invitiamo a utilizzare i consueti canali per contattare la tua compagnia assicurativa. + Tessera sanitaria e PIN Solo PIN Registrazione nell\'app di prescrizione elettronica Il campo del nome non può essere vuoto. @@ -224,117 +224,117 @@ profilo %s selezionato colore di sfondo - grigio primaverile - drosera + Grigio primaverile + Drosera Esso! È! Rosa! Albero - Luna Blu Settembre + Luna Blu di settembre Accesso non effettuato Legati insieme - Ultima connessione il %s - Eliminare profilo? - Questa operazione cancellerà tutti i dati del profilo su questo dispositivo. Le tue prescrizioni nella rete sanitaria rimarranno intatte. - Spegnere - Interrompere + Ultimo collegamento il %s + Eliminare il profilo? + Questa operazione eliminerà tutti i dati dal profilo su questo dispositivo. Le tue prescrizioni nella rete sanitaria verranno conservate. + Eliminare + Annulla eliminare il profilo Vuoi eliminare l\'ultimo profilo. L\'app richiede almeno un profilo. Inserisci un nome per il nuovo profilo. Errore 20 10 76831 - Non è stato possibile raggiungere l\'elenco delle tessere sanitarie. Per favore riprova. - Puoi trovare informazioni verificate da esperti su malattie, codici ICD e su temi di prevenzione e cura sul Portale Sanitario Nazionale. - Apri Gesund.bund.de - Abbiamo modificato l\'informativa sulla privacy - L\'app per la prescrizione elettronica si è evoluta. Ciò ha reso necessario aggiornare la nostra politica sulla privacy. + Impossibile raggiungere l\'elenco delle tessere sanitarie. Per favore riprova. + Puoi trovare informazioni verificate da esperti su malattie, codici ICD e argomenti di prevenzione e cura nel Portale sanitario nazionale. + Apri healthy.bund.de + Abbiamo cambiato la politica sulla privacy + L’app di prescrizione elettronica si è evoluta. Ciò ha reso necessario aggiornare la nostra politica sulla privacy. Apri l\'informativa sulla privacy - Questo è cambiato dal %s : + Questo è cambiato da %s : Cosa succede quando apri l\'app? - Cosa succede se utilizzo la funzione fotocamera / leggo le ricette con la fotocamera? + Cosa succede se utilizzo la funzione fotocamera/leggo ricette con la fotocamera? Nessuna nuova ricetta disponibile - %s nuova ricetta + La nuova ricetta %s %s nuove ricette Riscattabile - Nel riscatto + Nella redenzione Redento Sconosciuto Visualizza i log di accesso Chi ha avuto accesso alle tue ricette e quando? Chiave di accesso al servizio di prescrizione - log di accesso + Accedi ai log Nessun registro di accesso - Non ci sono ancora registri di accesso. - La ricetta è attualmente in lavorazione e non può essere cancellata + Non ci sono ancora log di accesso. + La ricetta è attualmente in elaborazione e non può essere eliminata Accettare - A quanto pare non ha funzionato - Siamo consapevoli che il collegamento con la tessera sanitaria ha le sue insidie. In futuro, quindi, la registrazione dovrebbe essere possibile anche tramite un\'app di assicurazione sanitaria già autenticata. \n\n Stiamo anche lavorando per consentire il riscatto digitale delle prescrizioni senza registrazione. \n\n Hai notato qualcosa durante questo processo che vorresti condividere con noi? Per favore scrivici, siamo anche felici di ricevere feedback molto critici. + Apparentemente non ha funzionato + Siamo consapevoli che il collegamento con la tessera sanitaria ha le sue insidie. In futuro la registrazione dovrebbe essere possibile anche tramite un’app dell’assicurazione sanitaria già autenticata. \n\n Stiamo anche lavorando per garantire che le prescrizioni possano essere riscattate digitalmente senza registrarsi. \n\n Hai notato qualcosa durante questo processo che vorresti condividere con noi? Scriveteci, saremo lieti di ricevere anche feedback molto critici. Suggerimenti per la connessione Migliora la forza della connessione Se necessario, rimuovere la copertura protettiva. - Se il dispositivo vibra e quindi interrompe la connessione, cercare la posizione ottimale entro un raggio ridotto. - Muovi il dispositivo sulla mappa molto lentamente. - Posizionare il dispositivo direttamente sulla scheda. - Per fare ciò appoggiare la tessera sanitaria su una superficie piana (es. un tavolo). + Se il dispositivo vibra e la connessione si interrompe, cercare la posizione ottimale entro un raggio ristretto. + Muovi il dispositivo molto lentamente sulla mappa. + Posiziona il dispositivo direttamente sulla scheda. + Per farlo appoggia la tessera sanitaria su una superficie piana (ad esempio un tavolo). Migliora la forza della connessione - Notare il posizionamento del sensore NFC - Scopri dove si trova il sensore NFC nel tuo dispositivo (qui, ad esempio, una panoramica dei dispositivi di %s ). - In alcuni casi, la posizione del sensore NFC può differire all\'interno di una serie di modelli (qui, ad esempio, le informazioni per %s ). + Da notare il posizionamento del sensore NFC + Scopri dove si trova il sensore NFC nel tuo dispositivo (qui ad esempio una panoramica dei dispositivi di %s ). + In alcuni casi all\'interno di una serie di modelli la posizione del sensore NFC può differire (qui ad esempio l\'informazione per %s ). Prossimo Consiglio Ulteriore Vicino - Provare + Tentativo scrivici - Licenza di ricerca farmacia - riscattare - Prescrizione scansionata - Scansionato su %s - Contrassegnato come riscattato su %s + Ricerca farmacia con licenza + Riscattare + Ricetta scannerizzata + Scansionato il %s + Contrassegnato come riscattato il %s Come vuoi continuare? Ordine Disponibile a breve - Prenota ora per il ritiro o fallo consegnare tramite corriere o spedizione - Salva per ordine successivo + Prenota ora per il ritiro o ricevilo tramite corriere o spedizione + Risparmia per un ordine successivo Salva le ricette sul dispositivo Continua con la ricetta %s Continua con %s ricette - Impossibile collegare la tessera sanitaria - Il profilo attuale è già collegato ad un\'altra tessera sanitaria (numero di assicurazione sanitaria %s ). - La tua tessera sanitaria è già collegata ad un altro profilo. Passa al profilo %s . - Salva sul computer - recapiti e indirizzo + Connessione tessera sanitaria fallita + Il profilo attuale è già collegato ad un\'altra tessera sanitaria (numero cassa malati %s ). + La tua tessera sanitaria è già collegata ad un altro profilo. Vai al profilo %s . + Salva + Dettagli di contatto e indirizzo Contatto - numero di telefono - Si prega di fornire un numero di telefono per il contatto. - Indirizzo di posta (facoltativo) + Numero di telefono + Si prega di fornire un numero di telefono per contattarci. + Indirizzo e-mail (facoltativo) indirizzo di consegna - nome e cognome - Si prega di inserire un nome e cognome per scopi di contatto. + Nome e cognome + Si prega di fornire un nome e cognome per contattarci. Via e numero civico - Inserisci una via e un numero civico per essere ricontattato. + Si prega di fornire una via e un numero civico per contattarci. Indirizzo aggiuntivo (facoltativo) - Istruzioni di consegna (facoltativo) - Ulteriori informazioni di contatto richieste + Istruzioni per la consegna (facoltative) + Sono richiesti ulteriori dettagli di contatto Non salvare le modifiche? scartare - Per la ricerca, la directory della farmacia utilizza le coordinate geografiche che sono state determinate con l\'aiuto di OpenStreetMap. Ringraziamo il progetto per questo aiuto. - © OpenStreetMap ( %s ) + Per la ricerca l\'elenco delle farmacie utilizza le coordinate geografiche determinate con l\'aiuto di OpenStreetMap. Ringraziamo il progetto per questo aiuto. + ©OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Privacy e utilizzo + Protezione e utilizzo dei dati Ulteriore - Hai ricevuto il PIN in una lettera dalla tua compagnia di assicurazione sanitaria. + Hai ricevuto il PIN tramite lettera dalla tua cassa malati. Nessun PIN ricevuto Codice PIN - Controlla la connessione a Internet e l\'impostazione di ora/data del tuo dispositivo. + Controlla la connessione Internet del tuo dispositivo e le impostazioni di data/ora. Per accedere, premere \"Sblocca\". - chiuso fuori? Verifica le tue credenziali biometriche su questo dispositivo. + Chiuso fuori? Verifica le tue credenziali biometriche su questo dispositivo. Ha dimenticato la password? Elimina l\'app e reinstallala. Puoi scoprire perché nel nostro %s . - zona di aiuto - confezione e unità + zona aiuto + Dimensioni della confezione e unità principio attivo Quantità di principio attivo - designazione del lotto + Nome del lotto Esp categoria Vaccino @@ -343,183 +343,183 @@ Avviso Aiutaci a migliorare questa app Inserire la password - La password deve essere lunga almeno otto caratteri - Sicurezza della password non sufficiente - Sicurezza della password sufficiente + La password deve contenere almeno otto caratteri + La forza della password non è sufficiente + Forza della password sufficiente La password è visibile La password non è visibile biometrica parola d\'ordine - aspettando una risposta - Nessuna prescrizione - Al momento non hai prescrizioni rimborsabili. + In attesa di risposta + Nessuna ricetta + Al momento non hai prescrizioni riscattabili. Aggiornare Disconnessione automatica - Per motivi di sicurezza, la connessione al server delle ricette viene interrotta dopo 12 ore. Riconnettiti per ottenere le ricette correnti. + Per motivi di sicurezza il collegamento al server delle ricette viene interrotto dopo 12 ore. Riconnettiti per ottenere le ricette attuali. Collegare - Hai ricevuto una copia cartacea? + Hai ricevuto una stampa cartacea? Aggiungi ricette alla tua lista toccando il pulsante di scansione nell\'angolo in alto a destra. - Eseguire la scansione della stampa cartacea - Devi essere loggato per ricevere le ricette automaticamente. + Scansione di stampe cartacee + Per ricevere le prescrizioni automaticamente è necessario effettuare il login. Registrati Nessuna prescrizione riscattata Le tue prescrizioni riscattate vengono visualizzate qui. Per motivi di protezione dei dati, le tue ricette verranno cancellate dal server delle ricette dopo 100 giorni. Nessuna prescrizione riscattata - Le tue prescrizioni riscattate vengono visualizzate qui. Aggiungi prescrizioni tramite scansione per iniziare a riscattare. - gestione dei dispositivi + Le tue prescrizioni riscattate vengono visualizzate qui. Aggiungi ricette tramite scansione per iniziare a riscattare. + Gestione dei dispositivi Dispositivi connessi - Registrato da %s (questo dispositivo) - Registrato da %s - Per motivi di sicurezza, la connessione al server delle ricette viene interrotta dopo 12 ore. Per riconnettersi servono tessera sanitaria e PIN per ogni processo di connessione. + Registrato dal %s (questo dispositivo) + Registrato dal %s + Per motivi di sicurezza il collegamento al server delle ricette viene interrotto dopo 12 ore. Per riconnettersi avrai bisogno della tessera sanitaria e del PIN per ogni procedura di connessione. Codice PIN - Inserisci il tuo PIN (tessera sanitaria). + Inserisci il PIN (tessera sanitaria). Ulteriore Registrati Dispositivi connessi - rimuovere il dispositivo? - Interrompere - RIMOSSO + Rimuovere il dispositivo? + Annulla + Rimuovere Rimuovere questo dispositivo? Vuoi rimuovere %s ? - Se rimuovi %s , la connessione al server della ricetta verrà disconnessa definitivamente entro 12 ore al massimo. - Caricamento dispositivi in corso... + Se rimuovi %s , la connessione al server delle ricette verrà interrotta definitivamente entro 12 ore al massimo. + I dispositivi stanno caricando... Nessun dispositivo Non ci sono dispositivi collegati a questa tessera sanitaria. Riprova Uh Oh :-( Impossibile caricare l\'elenco dei dispositivi. - wwweg… + wwweg... Nessuna connessione internet. Medicinali e medicazioni - stupefacenti - Consegna di farmaci soggetti a prescrizione ai sensi del § 4 AMVV + Narcotici + Dispensazione di medicinali soggetti a prescrizione ai sensi del paragrafo 4 AMVV Hai bisogno di aiuto? - Abbiamo raccolto alcuni consigli per risolvere i problemi più comuni. - Avvia suggerimenti per la connessione - sbloccare + Abbiamo raccolto per te alcuni suggerimenti per risolvere i problemi più comuni. + Avvia i suggerimenti per la connessione + Sbloccare carta bloccata Il PIN è stato inserito in modo errato per tre volte. La tua carta è stata quindi bloccata per motivi di sicurezza. - carta di sblocco + Sblocca la carta Inserisci PUK - Con il tuo PIN hai ricevuto un PUK a 8 cifre dalla tua compagnia assicurativa. - Scegli nuovo PIN + Insieme al PIN avete ricevuto dalla vostra compagnia assicurativa un PUK di 8 cifre. + Scegli il nuovo PIN Puoi scegliere tu stesso il tuo nuovo numero di identificazione personale (PIN) (da 6 a 8 cifre). - PIN ricordato? - Prendi nota del PIN e conservalo in un luogo sicuro. - Interrompere + Hai ricordato il tuo PIN? + Si prega di annotare il PIN e di conservarlo in un luogo sicuro. + Annulla OK - Sblocco non possibile - Hai raggiunto il numero massimo di carte sbloccate con questo PUK o lo hai inserito ripetutamente in modo errato. Si prega di contattare la propria compagnia assicurativa. + Lo sblocco non è possibile + Hai raggiunto il numero massimo di sblocchi della carta con questo PUK o lo hai inserito ripetutamente in modo errato. Ti invitiamo a contattare la tua compagnia assicurativa. Puoi utilizzare un PUK per un massimo di 10 sblocchi. - carta sbloccata + Carta sbloccata Quello che ti serve: - la tua tessera sanitaria + La tua tessera sanitaria PUK della tua tessera sanitaria Ulteriore - tessera sanitaria - Ordine PIN o carta + Tessera sanitaria + Ordina PIN o carta Registrati Come vuoi accedere? Tessera sanitaria abilitata NFC - PIN per la tessera sanitaria - Non hai ancora tessera sanitaria e PIN abilitati NFC? + PIN della tessera sanitaria + Non hai ancora la tessera sanitaria e il PIN abilitati NFC? Applica ora - Oppure: Accedi con %s . + Oppure: accedi con %s . La tua app per l\'assicurazione sanitaria - "Il tuo numero di accesso si trova nell\'angolo in alto a destra della tua tessera sanitaria." + "Trovi il tuo numero di accesso nell\'angolo in alto a destra della tua tessera sanitaria." La mia carta non ha un numero di accesso - Hai altri %s tentativi prima che la tua carta venga bloccata. - Hai altri %s tentativi prima che la tua carta venga bloccata. + Hai ancora %s tentativo prima che la tua carta venga bloccata. + Hai ancora %s tentativi prima che la tua carta venga bloccata. - Metti la tessera sanitaria sul retro del telefono - Il seguente processo può richiedere fino a 30 secondi. + Posiziona la tessera sanitaria sul retro del telefono + Il seguente processo potrebbe richiedere fino a 30 secondi. Posiziona la carta %s sul retro del telefono. - nell\'angolo in alto a destra - nella parte centrale superiore - in alto a sinistra + nella zona in alto a destra + al centro nella zona superiore + nella zona in alto a sinistra nella zona centrale a destra mezzo - nel centrosinistra + nella zona centrale a sinistra nella zona in basso a destra - in basso al centro - in basso a sinistra + al centro nella zona inferiore + nella zona in basso a sinistra Aiuto Inviato %s minuti fa Inviato il %s Inviato proprio ora - Inviato alle %s in punto + Inviato alle %s ora Non più valido - Accedi con l\'app - scegli l\'assicurazione - Non hai trovato quello che stavi cercando? Questo elenco viene costantemente ampliato. La registrazione con tessera sanitaria è già supportata da tutte le compagnie di assicurazione sanitaria. + Registrati con l\'app + Scegli l\'assicurazione + Non hai trovato quello che cercavi? Questo elenco viene costantemente ampliato. La registrazione con la tessera sanitaria è già supportata da ogni cassa malati. Feedback dall\'app di prescrizione elettronica - Non vediamo l\'ora di ricevere il tuo feedback. Si prega di utilizzare lo spazio sottostante e di essere il più precisi possibile: + Non vediamo l\'ora di ricevere il tuo feedback. Si prega di utilizzare il seguente spazio e di essere il più precisi possibile: PUK Vicino Che peccato… - Sfortunatamente, il tuo dispositivo non soddisfa i requisiti minimi per accedere all\'app di prescrizione elettronica. Per l\'autenticazione sicura con la tessera sanitaria sono necessari almeno Android 7 e un chip NFC. + Purtroppo il tuo dispositivo non soddisfa i requisiti minimi per la registrazione nell\'app e-prescription. Per l\'autenticazione sicura con la tessera sanitaria sono necessari almeno Android 7 e un chip NFC. Saperne di più - Salvare i dati di accesso? - Salva sul computer + Vuoi salvare i dati di accesso? + Salva Non salvare Avviso - Per motivi di sicurezza, la connessione al server delle ricette viene interrotta dopo 12 ore. Per riconnettersi servono tessera sanitaria e PIN per ogni processo di connessione. - Imposta la sicurezza biometrica - Impossibile salvare i dati di accesso. Imposta in anticipo la sicurezza biometrica (ad es. impronta digitale) sul tuo dispositivo. - Interrompere - Idee + Per motivi di sicurezza il collegamento al server delle ricette viene interrotto dopo 12 ore. Per riconnettersi avrai bisogno della tessera sanitaria e del PIN per ogni procedura di connessione. + Configura la sicurezza biometrica + Non è possibile salvare i dati di accesso. Configura innanzitutto la sicurezza biometrica (ad esempio l\'impronta digitale) sul tuo dispositivo. + Annulla + Impostazioni Avviso Accettare - Sicurezza dei tuoi dati di prescrizione - \"Questa app utilizza il sensore biometrico più sicuro fornito dal tuo dispositivo per archiviare le tue credenziali in un\'area sicura della memoria del dispositivo.\" - La sicurezza biometrica dei tuoi dati di accesso ti consente di aprire in futuro questa app senza inserire PIN e tessera sanitaria e di visualizzare, richiamare, riscattare o cancellare le prescrizioni. - Assicurati che anche le persone con cui potresti condividere questo dispositivo e le cui caratteristiche biometriche potrebbero essere memorizzate su questo dispositivo abbiano accesso alle tue prescrizioni. + Sicurezza dei dati della tua prescrizione + \"Questa app utilizza il sensore biometrico più sicuro fornito dal tuo dispositivo per proteggere le tue credenziali in un\'area protetta della memoria del dispositivo.\" + La sicurezza biometrica dei tuoi dati di accesso ti permetterà di aprire in futuro questa app, visualizzare, recuperare, riscattare o cancellare prescrizioni senza tessera sanitaria e inserendo il PIN. + Assicurati che anche le persone con cui condividi questo dispositivo e le cui caratteristiche biometriche potrebbero essere memorizzate su questo dispositivo abbiano accesso alle tue prescrizioni. che purtroppo non ha funzionato - L\'autenticazione con l\'app della cassa malati non è riuscita. + L\'autenticazione con l\'app dell\'assicurazione sanitaria non è riuscita. Scaduto il %s - La ricetta è già stata cancellata dal server - Correggi l\'input o annulla le modifiche + La ricetta è già stata eliminata dal server + Correggi la tua voce o annulla le modifiche Corretto - dati assicurati + Dati dell\'assicurato Cognome Assicurazione - numero di polizza - numero di accesso alla carta + Numero di polizza + Numero di accesso alla carta Registrati - Annulla registrazione - Salva sul computer + Disconnettersi + Salva Modifica Modifica la foto profilo Ulteriore il server non risponde Per favore riprova più tardi. Riprova - Cerca un\'assicurazione - Connettersi ora al server delle ricette? + Cerca l\'assicurazione + Connettersi al server delle ricette adesso? Accesso effettuato con successo Collegamento perso - Connettersi ora al server delle ricette? + Connettersi al server delle ricette adesso? Nessun gettone - Riceverai un token quando avrai effettuato l\'accesso al servizio di prescrizione.\n - ordini - Seleziona il PIN desiderato - carta di sblocco - Scegli PIN - Ripetere PIN + Riceverai un token quando avrai effettuato l\'accesso al servizio di prescrizione.\n + Ordini + Selezionare il PIN desiderato + Sblocca la carta + Seleziona PIN + Ripeti PIN Le voci differiscono l\'una dall\'altra. Nessun ordine - Non hai ancora nessun ordine. + Non hai ancora alcun ordine. Proprio adesso Alle %s in punto Il carrello è pronto La ricetta è stata aggiunta al tuo carrello. Vai al sito web della farmacia per completare l\'ordine. - Apri carrello + Aprire il carrello Mostra questo codice di ritiro in farmacia. - Ricevi il codice di ritiro + Codice di ritiro ricevuto Il messaggio non può essere visualizzato Contatta la tua farmacia ( %s ). - Mostra link al carrello + Mostra il collegamento al carrello Mostra il codice di ritiro Mostra il messaggio %s alle %s in punto @@ -527,50 +527,50 @@ Panoramica dell\'ordine Nuovo Corso - Ordine - Gratuito per il chiamante. Orari di servizio: Lun - Ven 8:00 - 20:00 escluse le festività nazionali + L\'ordine + Gratuito per il chiamante. Orari di servizio: lun - ven 8:00 - 20:00 esclusi festivi nazionali Farmacia - Seleziona il PIN desiderato + Selezionare il PIN desiderato PIN desiderato salvato Attualmente aperto e vicino a me - Filtra per... + Filtra per… Inizia la ricerca - affidamento diretto - farmacie - numero di telefono (facoltativo) + Assegnazione diretta + Farmacie + Numero di telefono (facoltativo) Cerca per nome o indirizzo Nessuna informazione valida sulla farmacia - Non sono state trovate informazioni aggiornate su questa farmacia. La voce per questa farmacia verrà cancellata. + Non è stata trovata alcuna informazione attuale su questa farmacia. La voce relativa a questa farmacia verrà eliminata. OK Elenco delle farmacie non disponibile - Al momento non è possibile recuperare informazioni aggiornate su questa farmacia. Per favore controlla la tua connessione Internet. - Interrompere + Al momento non è possibile accedere alle informazioni aggiornate su questa farmacia. Per favore controlla la tua connessione Internet. + Annulla Riprova - Salva Ambiente - Non è possibile accedere - Sembra che le tue credenziali di accesso biometrico siano cambiate. Si prega di registrarsi nuovamente con la propria tessera sanitaria. - Interrompere + Salva ambiente + L\'accesso non è possibile + Sembra che le tue caratteristiche di accesso biometriche siano cambiate. Effettua nuovamente l\'accesso con la tua tessera sanitaria. + Annulla Registrati - profilo 1 + Profilo 1 Vicino a me - Riscattabile in seguito + Riscattabile più tardi Riscattabile da %s - miglioramenti del prodotto + Miglioramenti del prodotto Analisi anonima - Aiutaci a migliorare questa app. Tutti i dati di utilizzo vengono raccolti in forma anonima e vengono utilizzati solo per migliorare l\'esperienza dell\'utente. - sicurezza del dispositivo + Aiutaci a migliorare questa app. Tutti i dati di utilizzo vengono raccolti in forma anonima e vengono utilizzati esclusivamente per migliorare l\'esperienza dell\'utente. + Sicurezza del dispositivo impostazioni personali Accessibilità - miglioramenti del prodotto - Ricetta aggiunta + Miglioramenti del prodotto + Aggiunta ricetta Ricetta già disponibile Si è verificato un errore durante l\'importazione - Spegnere - Prescrizione scansionata - Sostituzione possibile + Eliminare + Ricetta scannerizzata + Possibile preparazione sostitutiva PIN dimenticato - %s ricetta + %s Ricetta %s Ricette Ho letto e accetto l\'informativa sulla privacy e i termini di utilizzo. @@ -579,157 +579,156 @@ Vorremmo: Migliora l\'usabilità. Rileva errori e arresti anomali. - Tutti i dati sono naturalmente raccolti in forma anonima. - Puoi modificare questa decisione nelle impostazioni di sistema in qualsiasi momento. + Tutti i dati vengono ovviamente raccolti in forma anonima. + Puoi modificare questa decisione in qualsiasi momento nelle impostazioni di sistema. Continua Accettare Questa app utilizza il metodo più sicuro fornito dal tuo dispositivo. - Salva sul computer + Salva Scegliere farmaco - nome depositato + Nome depositato NO dosaggio data di rilascio Questa prescrizione verrà riscattata per te come parte di un trattamento. Non specificato - pagamento aggiuntivo + Pagamento aggiuntivo farmaco - Bolle di consegna + Istruzioni per l\'invio Idoneo secondo LPP - preparazione alternativa - nome della ricetta + Preparazione alternativa + Nome della ricetta Confezione - istruzione di lavorazione + Istruzioni per la produzione Descrizione dato da rilasciato il: principio attivo - prescritta + Prescritta Ricevere Che cos\'è un incarico diretto? - In caso di invio diretto, una prescrizione di uno studio o di un ospedale viene riscattata direttamente in farmacia. Gli assicurati non devono intraprendere alcuna azione e non possono intervenire nel processo di riscatto. \n\n I referral diretti sono elencati nell\'app di prescrizione elettronica per rendere il trattamento più trasparente per te. - tariffa del servizio di emergenza - A volte è necessaria la fretta. Alcune prescrizioni possono essere riscattate senza il pagamento aggiuntivo di una tariffa per il servizio di emergenza, ad esempio di notte o nei giorni festivi. + Con la prescrizione diretta, una prescrizione da uno studio o da un ospedale viene compilata direttamente in farmacia. Gli assicurati non devono intraprendere alcuna azione e non possono intervenire nel processo di riscatto. \n\n I referral diretti sono elencati nell\'app di prescrizione elettronica per rendere il trattamento più trasparente per te. + Tassa per il servizio di emergenza + A volte la fretta è necessaria. Alcune prescrizioni possono essere soddisfatte senza il pagamento aggiuntivo di una tariffa per il servizio di emergenza, ad esempio di notte o nei giorni festivi. Farmaci soggetti a ticket - Esente da partecipazione al pagamento - Coloro che hanno un\'assicurazione sanitaria obbligatoria devono pagare una partecipazione ai costi fino a dieci euro per i farmaci da prescrizione. \n\n L\'importo del ticket dipende dal prezzo dei medicinali. Devi pagare tu stesso i farmaci che costano meno di 5€.\n Per i medicinali più costosi, devi pagare il dieci per cento del prezzo, ma almeno 5€ e non più di 10€. \n\n I bambini ei giovani di età inferiore ai 18 anni sono generalmente esentati dal ticket. \n\n Se i tuoi costi annuali per i farmaci superano il tuo limite finanziario, puoi essere esentato dalla partecipazione ai costi. Parlane con la tua cassa malati. - Sei esente dal co-pagamento di questo farmaco. La tua assicurazione sanitaria coprirà il costo del farmaco. + Esente da pagamento aggiuntivo + Quelli con l\'assicurazione sanitaria pubblica devono pagare un pagamento aggiuntivo fino a dieci euro per i farmaci soggetti a prescrizione. \n\n L\'importo del pagamento aggiuntivo dipende dal prezzo del farmaco. I farmaci che costano meno di 5 euro li devi pagare tu.\n Per i medicinali più costosi bisogna pagare il 10% del prezzo, ma almeno 5 euro e un massimo di 10 euro. \n\n I bambini e i giovani sotto i 18 anni sono generalmente esenti dal pagamento aggiuntivo. \n\n Se i costi annuali per i farmaci superano il limite dell’onere finanziario, puoi essere esentato dalla partecipazione ai costi. Parlane con la tua compagnia di assicurazione sanitaria. + Sei esente dal pagamento di un ticket per questo farmaco. La vostra compagnia di assicurazione sanitaria coprirà il costo del farmaco. Per quanto tempo è valida questa prescrizione? Durante questo periodo puoi riscattare la tua ricetta in qualsiasi farmacia con un pagamento aggiuntivo massimo di 10€. - Sostituzione possibile - A causa dei requisiti legali della tua compagnia di assicurazione sanitaria, puoi ricevere un\'alternativa con lo stesso principio attivo. \n\n I medicinali possono avere un aspetto ed essere chiamati in modo diverso, hanno prezzi e produttori diversi, ma contengono sempre lo stesso principio attivo. Il principio attivo stesso e il dosaggio sono particolarmente importanti per l\'effetto dei farmaci nel corpo. I pazienti in farmacia spesso ricevono un farmaco diverso da quello prescritto dal medico sulla prescrizione, a condizione che i farmaci siano comparabili. Ci possono essere ragioni terapeutiche ed economiche per il cambiamento. - Prescrizione scansionata - Per motivi di sicurezza, le prescrizioni importate da una stampa cartacea non devono riportare alcun dato personale o medico. \n\n Accedi a questa app con tessera sanitaria o app assicurativa per visualizzare tutte le informazioni contenute nella ricetta. + Possibile preparazione sostitutiva + A causa dei requisiti legali della vostra compagnia di assicurazione sanitaria, potrebbe esservi offerta un\'alternativa con lo stesso principio attivo. \n\n I medicinali possono apparire ed essere chiamati in modo diverso, hanno prezzi e produttori diversi, ma contengono comunque lo stesso principio attivo. Il principio attivo stesso e il dosaggio sono cruciali per l’effetto dei medicinali nel corpo. Spesso i pazienti ricevono in farmacia un farmaco diverso da quello prescritto dal medico, a condizione che il farmaco sia comparabile. Potrebbero esserci ragioni terapeutiche ed economiche per il cambiamento. + Ricetta scannerizzata + Le prescrizioni importate da una copia cartacea non possono visualizzare informazioni personali o mediche per motivi di sicurezza. \n\n Accedi a questa app con tessera sanitaria o app Assicurazione per visualizzare tutte le informazioni contenute nella ricetta. Ricetta errata Questa prescrizione è stata emessa in modo errato. - Prescrizione scansionata - tariffa del servizio di emergenza + Tassa per il servizio di emergenza Dosaggio secondo istruzioni scritte telefono - luogo + sito web Posta - Ordinamento per distanza non possibile. + L\'ordinamento per distanza non è possibile. OK Inserisci il PIN attuale - PIN errato inserito + È stato inserito un PIN errato Il PIN attuale della tua tessera sanitaria carta bloccata Sblocca la tua carta in Impostazioni > Sblocca carta. Per motivi di sicurezza, inserisci il tuo PIN attuale. PIN dimenticato - Ricetta sbagliata + Ricetta errata farmaco - Qualcosa sembra essere andato storto durante la creazione della tua ricetta. Segnalare un errore? + Sembra che qualcosa sia andato storto durante la creazione della tua ricetta. Segnala un errore? Rapporto Accesso non effettuato Registrato con - tessera sanitaria + Tessera sanitaria biometrica Accesso non effettuato - Siamo interessati alla tua opinione. Ti preghiamo di dedicare cinque minuti per rispondere al nostro sondaggio. Grazie in anticipo. - avviso di avvertimento + Siamo interessati alla tua opinione. Ti preghiamo di dedicare cinque minuti per completare il nostro sondaggio. Grazie mille in anticipo. + Avviso di avvertenza Farmacia aggiunta ai preferiti Farmacia rimossa dai preferiti Le mie farmacie - Sicurezza della password molto buona + Forza della password molto buona Operazione di scrittura non riuscita Impossibile salvare il PIN Rapporto Assegna PIN Regola di accesso violata - Non sei autorizzato ad accedere alla directory della mappa. - Assegna il tuo pin - La carta è protetta da un PIN della tua compagnia di assicurazione sanitaria (PIN di trasporto), si prega di assegnare il proprio PIN. + Non hai l\'autorizzazione per accedere alla directory della mappa. + Assegna il tuo PIN + La carta è protetta con un PIN della vostra cassa malati (PIN per i trasporti), inserite il vostro PIN personale. Password non trovata - Non c\'è nessuna password memorizzata sulla tua carta. + Sulla tua carta non è memorizzata alcuna password. Sei stato disconnesso - Accedi di nuovo per aggiornare le tue ricette. - numero di principio attivo + Accedi nuovamente per aggiornare le tue ricette. + Numero del principio attivo potenza e unità Riscattato %s minuti fa Riscattato il %s - Riscattato proprio ora + Redento proprio adesso Riscattato alle %s in punto Ordini - Questa prescrizione è stata riscattata per te come parte di un trattamento. - tariffa del servizio di emergenza - Questa prescrizione non può essere compilata di notte in una farmacia senza il pagamento aggiuntivo di una tassa per il servizio di emergenza. + Questa prescrizione è stata compilata come parte di un trattamento per te. + Tassa per il servizio di emergenza + Questa prescrizione non può essere compilata di notte in farmacia senza il pagamento aggiuntivo di una tassa per il servizio di emergenza. Cerca qui - Idee - Condividi la posizione nelle impostazioni. + Impostazioni + Condividi la posizione nelle Impostazioni. Vicino a me Tenere premuto per modificare il nome. Immettere il nuovo nome per il profilo. - Devi essere loggato per ricevere le prescrizioni digitali dal tuo studio. - Ricevi le ricette in digitale? - Trascina lo schermo verso il basso per aggiornare. - Nessuna prescrizione + Per ricevere le prescrizioni in formato digitale dal tuo studio, devi essere loggato. + Ricevere prescrizioni in formato digitale? + Abbassa lo schermo per aggiornare. + Nessuna ricetta Aggiungi ricette utilizzando il pulsante + nell\'angolo in alto a destra. Registrati - archivio prescrizioni + Archivio ricette Forse più tardi Registrati Modifica la foto profilo - archivio prescrizioni + Archivio ricette Inserisci il nome - Salva sul computer + Salva Il mio ordine Destinatario: dentro - ricette + Ricette Farmacia Inviare Modifica Ritiro in farmacia Consegna tramite corriere - Consegna per posta + Consegna per corrispondenza %s Ricette - Riscatto non possibile + Non è possibile riscattare Non è stato possibile riscattare una o più prescrizioni. - Nessuna ricetta selezionata - Per riscattare le ricette, è necessario selezionare almeno una ricetta. - Aggiungi informazioni di contatto + Nessuna ricetta scelta + Per riscattare le ricette è necessario selezionare almeno una ricetta. + Aggiungi i dettagli di contatto Modifica - Nessuna prescrizione - Al momento non hai prescrizioni rimborsabili + Nessuna ricetta + Al momento non hai prescrizioni riscattabili collezione ragazzo delle consegne Spedizione - scegli le ricette + Scegli le ricette Tocca qui per scansionare le ricette Premere a lungo per modificare i nomi - Aggiungi più profili, ad esempio per i tuoi figli o genitori - Fare clic sul display per saltare il suggerimento visualizzato. + Aggiungi profili aggiuntivi, ad esempio per i tuoi figli o genitori + Fare clic sul display per ignorare la descrizione comando visualizzata. Come riscattare? Come vorresti ricevere i tuoi farmaci? Riscatta direttamente - Riscatta i farmaci in loco + Riscattare i farmaci in loco Ordine - Prenota o fatti consegnare + Prenota o ricevilo Pronto - codice collettivo - codici singoli + Codice di raccolta + Codici individuali Hai %s ricetta. Hai %s ricette. @@ -745,102 +744,128 @@ Saperne di più Informazioni sullo scanner del codice della ricetta Quali dati contiene il codice della ricetta? - Il codice della ricetta contiene solo un identificatore della ricetta. Ciò consente di reperire la prescrizione sul servizio di prescrizione della rete sanitaria digitale. Il codice di prescrizione non contiene dati su di te o sui tuoi farmaci. - Quindi nessuno può fare nulla solo con il codice della ricetta? - Corretto. I dati della prescrizione devono essere scaricati dal servizio di prescrizione. Ciò richiede un accesso sicuro. - Chi può registrarsi al servizio di prescrizione? - L\'iscrizione al servizio di prescrizione nella rete sanitaria digitale è possibile per assicurati, farmacie, studi medici e ospedali. - Perché l\'app per la prescrizione elettronica utilizza le funzionalità di Google? - Google offre funzioni che possono essere facilmente integrate nelle app e che vengono costantemente sviluppate e aggiornate da Google. Ciò garantisce che le funzioni funzionino su molti dispositivi terminali diversi e possano essere utilizzate in modo sicuro. L\'app utilizza una funzione per migliorare la fotocamera e la funzionalità di scansione per i dispositivi Android (Google ML Kit). - Come funziona il miglioramento della scansione di Google ML Kit? - Google ML Kit aiuta a ottimizzare l\'immagine catturata da una fotocamera in modo che i codici delle ricette possano essere letti anche in condizioni di scarsa illuminazione o con modelli di fotocamere meno recenti. - I dati sulla prescrizione o sul mio farmaco verranno trasmessi a Google? - NO. Il codice della ricetta letto viene salvato direttamente nell\'app. Non verrà trasmesso a Google. I dati di prescrizione non sono memorizzati nel codice, solo nella rete sanitaria digitale. Da lì vengono inviati all\'app. Google non ha accesso alla rete sanitaria digitale. + Il codice ricetta contiene solo un identificatore per la ricetta. Ciò significa che la prescrizione può essere trovata sul servizio di prescrizione nella rete sanitaria digitale. Il codice di prescrizione non contiene alcuna informazione su di te o sui tuoi farmaci. + Quindi nessuno può fare nulla da solo con il codice della ricetta? + Corretto. I dati della prescrizione devono essere scaricati dal servizio prescrizione. A questo scopo è necessario un login sicuro. + Chi può iscriversi al servizio di prescrizione? + La registrazione per il servizio di prescrizione nella rete sanitaria digitale è possibile per gli assicurati, le farmacie, gli ambulatori e gli ospedali. + Perché l\'app di prescrizione elettronica utilizza le funzionalità di Google? + Google offre funzioni che possono essere facilmente integrate nelle app e che Google sviluppa e aggiorna continuamente. Ciò garantisce che le funzioni funzionino su molti dispositivi diversi e possano essere utilizzate in sicurezza. L\'app utilizza una funzione per migliorare la fotocamera e la funzionalità di scansione per i dispositivi Android (Google ML Kit). + Come funziona il miglioramento della scansione con Google ML Kit? + Google ML Kit aiuta a ottimizzare l\'immagine catturata da una fotocamera in modo che i codici delle ricette possano essere letti anche in condizioni di scarsa illuminazione o con modelli di fotocamere più vecchi. + I dati sulla prescrizione o sui farmaci verranno condivisi con Google? + NO. Il codice ricetta letto viene salvato direttamente nell\'app. Non sarà condiviso con Google. I dati della prescrizione non vengono memorizzati nel codice, ma solo nella rete sanitaria digitale. Da lì vengono trasmessi all\'app. Google non ha accesso alla rete sanitaria digitale. Quali dati elabora Google quando utilizza ML Kit? - Google ha accesso solo alle informazioni tecniche sul dispositivo finale utilizzato e sull\'uso generale della funzione aggiuntiva (ad es. Tasso di errore, impostazioni della fotocamera) per registrarle statisticamente e quindi migliorare la funzione aggiuntiva. Quando accedi, Google registra temporaneamente l\'indirizzo IP del tuo terminale. Le informazioni su di te e il contenuto della ricetta non verranno registrati da Google. - L\'uso di Google ML Kit è volontario? - SÌ. Tuttavia, ML Kit è integrato nello scanner del codice della ricetta nella versione Android dell\'app di prescrizione elettronica. Se si utilizza lo scanner del codice ricetta su un dispositivo Android, viene sempre utilizzata anche la funzione ML Kit. Tuttavia, puoi fare a meno di utilizzare lo scanner del codice della ricetta. Le tue prescrizioni possono essere caricate in app anche se ti registri alla rete sanitaria digitale con la tessera sanitaria elettronica o tramite la tua app della cassa malati. + Google ha accesso solo alle informazioni tecniche sul dispositivo utilizzato e sull\'utilizzo generale della funzione aggiuntiva (ad es. tasso di errore, impostazioni della fotocamera) per rilevarli statisticamente e quindi migliorare la funzione aggiuntiva. Durante l\'accesso, Google registra temporaneamente l\'indirizzo IP del tuo dispositivo. Le informazioni su di te e il contenuto della ricetta non vengono registrate da Google. + L\'utilizzo di Google ML Kit è volontario? + SÌ. Tuttavia, ML Kit è integrato nello scanner del codice della ricetta nella versione Android dell\'app di prescrizione elettronica. Se utilizzi lo scanner del codice ricetta su un dispositivo Android, viene sempre utilizzata la funzione ML Kit. Tuttavia, puoi evitare di utilizzare lo scanner del codice ricetta. Le vostre prescrizioni possono essere caricate anche nell’app se accedete alla rete sanitaria digitale con la tessera sanitaria elettronica o tramite l’app della vostra cassa malati. Posso vedere chi ha visualizzato le mie ricette? - SÌ. Tutti gli accessi ai tuoi dati sono completamente registrati nella rete sanitaria digitale. Nell\'app di prescrizione elettronica puoi vedere chi ha avuto accesso ai tuoi dati. - Chi posso contattare se ho domande sull\'app o sulla ricetta elettronica? - Puoi trovare informazioni dettagliate nella dichiarazione sulla protezione dei dati. + SÌ. Tutti gli accessi ai tuoi dati sono completamente registrati nella rete sanitaria digitale. Nell\'app e-prescrizione puoi vedere chi ha avuto accesso ai tuoi dati. + Dove posso contattare se ho domande sull\'app o sulla prescrizione elettronica? + Informazioni dettagliate si trovano nella dichiarazione sulla protezione dei dati. Numero di confezioni prescritte - Nessuna prescrizione - Per questo hai bisogno di prescrizioni rimborsabili. - scegli l\'assicurazione - Cerca un\'assicurazione - Interrompere + Nessuna ricetta + Per questo sono necessarie prescrizioni rimborsabili. + Scegli l\'assicurazione + Cerca l\'assicurazione + Annulla Per cosa vorresti candidarti? Per questa app è necessaria una carta e il PIN associato. - Come vorresti contattare la tua compagnia assicurativa? - La tua compagnia assicurativa offre le seguenti opzioni di contatto - La tua compagnia assicurativa offre le seguenti opzioni di contatto + Come desideri contattare la tua compagnia assicurativa? + La vostra compagnia assicurativa offre le seguenti possibilità di contatto + La vostra compagnia assicurativa offre la seguente possibilità di contatto Vicino PIN inserito in modo errato. Numero di accesso inserito in modo errato PUK inserito in modo errato. ricevute di spesa - Mostra le ricevute di spesa + Visualizza le ricevute dei costi ricevute di spesa Per ricevere le ricevute di spesa è necessario essere connessi al server. Collegare Nessuna ricevuta di spesa Disattivare - Interrompere - disabilitare la funzione - Questo eliminerà tutte le ricevute da questo dispositivo e dal server. - Ricevi le ricevute delle spese - Anche le tue ricevute di spesa vengono salvate sul server delle ricette. - Ricevere + Annulla + Disattiva la funzione + Ciò eliminerà tutte le ricevute di spesa da questo dispositivo e dal server. + Ricevere ricevute di spesa + Anche le vostre ricevute di spesa vengono salvate sul server delle ricette. + Ricevuto Totale: %s %s Scegliere Diviso - Spegnere - Spegnere + Eliminare + Eliminare Invia %s € prezzo totale - Suggerimento: invia le ricevute delle spese tramite l\'app dell\'assicurazione - Invia facilmente le ricevute dei costi tramite l\'app della tua compagnia assicurativa. Nel passaggio successivo, seleziona questa app e premi Condividi. + Suggerimento: inviate le ricevute dei costi tramite l\'app dell\'assicurazione + Invia facilmente le ricevute delle spese tramite l\'app della tua compagnia assicurativa. Nel passaggio successivo, seleziona questa app e premi Condividi. Pratica Farmacia Data mostra di più - ID farmaco + Identificazione del farmaco Rilasciato per KVNR: %s - Data di nascita: %s + Nato il: %s OK - Come si inviano le ricevute? - Trasferimento direttamente all\'app della tua compagnia assicurativa/ufficio di assistenza. Per fare ciò, seleziona l\'app nella pagina successiva. + Come si presentano i documenti giustificativi? + Passa direttamente all\'app del tuo ufficio assicurativo/previdenziale. Per fare ciò, seleziona l\'app nella pagina successiva. O - Salva il file e successivamente importalo nel portale assicurazioni/aiuti. + Salvare il file e successivamente importarlo nel portale assicurazioni/prestazioni. Articolo: %s - Numero: %s + Conteggio: %s IVA: %s %% Prezzo lordo in EUR: %s Tariffe aggiuntive - tariffa del servizio di emergenza - Commissione BTM + Tassa per il servizio di emergenza + Tariffa BTM Tassa di prescrizione T - costi di approvvigionamento + Costi di approvvigionamento Servizio di corriere Totale in EUR: %s prelievo Eliminare davvero? - Il file verrà eliminato dal tuo dispositivo e dal server. - Spegnere - Inserito + Il file verrà eliminato dal dispositivo e dal server. + Eliminare + Pubblicato Codice postale Posizione - Inserisci il tuo codice postale per contattarci. - Inserisci il tuo luogo di residenza quando ci contatti. - sarà riscattato per te - è stato riscattato per te + Fornisci il tuo codice postale per contattarci. + Indica il tuo luogo di residenza per contattarci. + Sarà riscattato per te + È stato riscattato per te Devi essere loggato per utilizzare questo servizio. - app assicurativa - tessera sanitaria - PIN associato richiesto + Applicazione assicurativa + Tessera sanitaria + È richiesto il PIN associato + Può essere riscattato solo domani come autopagante + Rimangono solo %s giorni per riscattare come pagatore autonomo + \nAncora riscattabile come pagatore autonomo per %s giorni\n + Valido solo per %s giorni + \nValido per %s giorni rimasti\n + Valido solo domani + Si applicano costi + Prende l\'assicurazione + Le ricette sono state trasferite con successo. + La ricetta non può essere elaborata. Per favore riprova. Potrebbe essere necessario scegliere una farmacia diversa. + La ricetta non può essere elaborata. La farmacia segnala un errore sconosciuto. Se necessario, prova un\'altra farmacia. + La ricetta è stata respinta dalla farmacia. La prescrizione potrebbe non essere valida oppure il tuo indirizzo di consegna o le informazioni di contatto potrebbero non essere validi. + Impossibile riscattare, controlla la tua connessione Internet. + La ricetta è stata trasferita con successo. La farmacia segnala però un errore di elaborazione. Si prega di contattare la farmacia. + La ricetta è stata respinta dalla farmacia. La prescrizione è già stata riscattata. + La ricetta è stata respinta dalla farmacia. La ricetta è stata cancellata. + Impossibile trasferire la ricetta. Per favore controlla la tua connessione Internet e prova di nuovo. + Impossibile trasferire una o più ricette. + Errore nell\'invio + Spedito con successo! + Errore in farmacia + Errore in farmacia + Contatta la farmacia + Prescrizione già riscattata + Ricetta eliminata + Senza internet Per ricevere i log di accesso, è necessario essere connessi al server. Entro questo termine potete comunque compilare la ricetta in farmacia, ma dovrete pagare voi stessi l\'intero prezzo d\'acquisto del medicinale. In alternativa potete chiedere al vostro studio di farvi riemettere la prescrizione. Pronto @@ -849,4 +874,13 @@ Nell\'app Fai scansionare questo codice in farmacia. Richiesta di correzione della fatturazione + farmaco + Inserisci almeno 1 carattere. + O. Prova l\'app in modalità demo + Modalità demo + Modalità demo + Utilizza la modalità demo + Modalità demo attivata + Finisci qui + Attiva la modalità demo diff --git a/android/src/main/res/values-night/colors.xml b/app/features/src/main/res/values-night/colors.xml similarity index 100% rename from android/src/main/res/values-night/colors.xml rename to app/features/src/main/res/values-night/colors.xml diff --git a/android/src/main/res/values-nl/strings.xml b/app/features/src/main/res/values-nl/strings.xml similarity index 56% rename from android/src/main/res/values-nl/strings.xml rename to app/features/src/main/res/values-nl/strings.xml index f6ef428c..4e139bd4 100644 --- a/android/src/main/res/values-nl/strings.xml +++ b/app/features/src/main/res/values-nl/strings.xml @@ -1,17 +1,17 @@ OK - Onderbreken - Opbrengst + Annuleren + Rug rondom Digitaal. Snel. Zeker. Taak-ID - toegangscode + Toegangscode Gebruiksvoorwaarden Gegevensbescherming - recepten - Cameratoegang geweigerd - Om de scanner te gebruiken, moet je de app toegang geven tot je camera in de systeeminstellingen. + Recepten + Toegang tot camera geweigerd + Om de scanner te gebruiken, moet u de app toegang geven tot uw camera in Systeeminstellingen. Richt de camera op een receptcode Dit is geen geldige receptcode Deze receptcode is al gescand @@ -19,148 +19,148 @@ %s recept herkend %s recepten herkend - Onderbreken - camera licht + Annuleren + Cameralicht Scannen annuleren? OK Annuleer niet - Daar gaan we + Laten we gaan Wat je nodig hebt: - Voer het toegangsnummer van de kaart in + Voer het kaarttoegangsnummer in voer de pincode in - probeer het nog eens + Probeer het nog eens Kan geen verbinding maken met de server. - Je hebt nog %s pogingen voordat je kaart wordt geblokkeerd. - Je hebt nog %s pogingen voordat je kaart wordt geblokkeerd. + U heeft %s nog één poging voordat uw kaart wordt geblokkeerd. + Je hebt nog %s meer pogingen voordat je kaart wordt geblokkeerd. - Het toegangsnummer vindt u rechtsboven op uw gezondheidskaart. - Onderbreken - Zoek naar kaart... + Het toegangsnummer vindt u rechtsboven op uw zorgkaart. + Annuleren + Zoek op kaart… Houd de gezondheidskaart tegen de achterkant van uw apparaat. Nog steeds aan het zoeken … - Verplaats de kaart langzaam aan de achterkant van het apparaat. + Beweeg de kaart langzaam aan de achterkant van het apparaat. Tip Apparaatbehuizingen kunnen het moeilijk maken om verbinding te maken via NFC. - kaart herkend + Kaart herkend Probeer de gezondheidskaart niet te verplaatsen. - Gezondheidskaart gevonden. Gelieve niet te bewegen. + Gezondheidskaart gevonden. Beweeg alsjeblieft niet. verbinding verbroken Houd uw gezondheidskaart opnieuw tegen de achterkant van het apparaat Versie: %s - Build-hash: %s - debug-menu + Bouw hash: %s + Debug-menu Open tot %s De hele dag geopend afdruk editor gematik GmbH\n Friedrichstraße 136\n 10117 Berlijn - Directeur: dr. medisch Markus Leyck-Dieken\n Registratierechtbank: arrondissementsrechtbank Berlijn-Charlottenburg\n Handelsregisternummer: HRB 96351\n Btw-identificatienummer: DE241843684 + Directeur: dr. med. Markus Leyck Dieken\n Registratierechtbank: arrondissementsrechtbank Berlijn-Charlottenburg\n KvK-nummer: HRB 96351\n Btw-identificatienummer: DE241843684 Verantwoordelijk voor de inhoud - Dr medisch Markus Leyck-Dieken + dr. med. Markus Leyck Dieken Contact Kennisgeving - We streven ernaar om genderneutraal taalgebruik te gebruiken. Als u fouten opmerkt, horen we graag van u via e-mail. - Duitslands moderne platform voor digitale geneeskunde - mail schrijven - website openen + We streven ernaar om gendergelijke taal te gebruiken. Mocht u fouten tegenkomen, dan horen wij dat graag per e-mail. + Het moderne platform van Duitsland voor digitale geneeskunde + Email schrijven + Website openen Welkom - Start registratie - ontgrendelen + Registratie starten + Ontgrendelen Register - Onderbreken + Annuleren Beveiliging - Juridisch + Legaal afdruk gegevensbescherming Gebruiksvoorwaarden - details + Details Markeer als ingewisseld - Markeer als niet ingewisseld - doseringsvorm - pakket grootte + Markeer als niet-ingewisseld + Doseringsvorm + Pakket grootte Verzekerd persoon Achternaam adres geboortedatum - Ziektekostenverzekering / betalers + Zorgverzekeraar/betaler toestand - verzekerings nummer - Voorschrijvende persoon + Verzekerings nummer + Voorschrijver Achternaam - Medische specialist - Artsennummer (LANR) + Gespecialiseerde arts + Doktersnummer (LANR) instelling Achternaam adres - Bedrijfspand nummer - telefoonnummer - Mail adres - ongeval op het werk - ongeval dag - Ongeval bedrijfs- of werkgeversnummer - Wil je dit recept permanent verwijderen? - Blussen - Onderbreken + Installatienummer + Telefoon nummer + E-mailadres + Arbeidsongeval + Dag van het ongeval + Ongevallenbedrijfs- of werkgeversnummer + Wil je dit recept definitief verwijderen? + Verwijderen + Annuleren openingstijden website Alleen vandaag inwisselbaar als zelfbetaler Register - Activeer NFC - Activeer de NFC-functie van uw toestel om in te loggen met uw gezondheidskaart. + Schakel NFC in + Activeer de NFC-functie van uw apparaat om in te loggen met uw gezondheidskaart. Activeren Juist Recepten ingewisseld? - Wilt u de recepten markeren als ingewisseld? + Wilt u de recepten als ingewisseld markeren? Niet ingewisseld Ingewisseld - Opent om %s + Opent om %s tijd +49 800 277 377 7 Technische hotline Open scanner voor recepten - Ideeën - Onderdruk schermafbeeldingen - Voorkomt de weergave van een miniatuur bij het schakelen tussen apps - Staat u toe dat e-recipe uw gebruiksgedrag anoniem analyseert? + Instellingen + Schermafbeeldingen onderdrukken + Voorkomt dat er een voorbeeldafbeelding wordt weergegeven bij het schakelen tussen apps + Staat u toe dat E-Prescription uw gebruiksgedrag anoniem analyseert? Technische informatie Beveiliging van uw receptgegevens - Zorg ervoor dat personen met wie u dit apparaat mogelijk deelt en van wie de biometrische kenmerken op dit apparaat kunnen worden opgeslagen, ook toegang hebben tot uw recepten. + Zorg ervoor dat mensen met wie u dit apparaat deelt en wier biometrische kenmerken mogelijk op dit apparaat zijn opgeslagen, ook toegang hebben tot uw recepten. verzenden is mislukt Geen e-mailprogramma ingesteld Geen resultaten We konden geen resultaten vinden voor deze zoekterm. - Open Source-licenties + Open source-licenties Contact Bel de technische hotline - Doe mee aan enquête + Neem deel aan de enquête +49 800 277 377 7 Ik wil helpen deze app beter te maken - Dit omvat hardware- en software-informatie op uw telefoon, instellingen voor de e-prescription-app en de hoeveelheid gebruik, maar nooit gegevens over u of uw gezondheid. - De gegevens worden alleen door de gegevensverwerker aan gematik GmbH ter beschikking gesteld en uiterlijk na 180 dagen verwijderd. U kunt de analyse op elk moment weer deactiveren in het app-menu. - Deze gegevens stellen ons in staat om te begrijpen welke functies vaak worden gebruikt en om deze te verbeteren. Verder kunnen we inschatten hoe lang oudere technologie ondersteund moet worden en wanneer we bijvoorbeeld een nieuwere versie van het besturingssysteem verplicht kunnen stellen zonder dat dit gevolgen heeft voor (te veel) gebruikers. - app verbeteren + Denk hierbij aan hardware- en softwaregegevens over uw telefoon, instellingen van de e-receptenapp en mate van gebruik, maar nooit gegevens over u of uw gezondheid. + De gegevens worden uitsluitend door de gegevensverwerker aan gematik GmbH ter beschikking gesteld en uiterlijk na 180 dagen verwijderd. U kunt de analyse op elk gewenst moment in het app-menu deactiveren. + Met deze gegevens kunnen we begrijpen welke functies vaak worden gebruikt en deze verbeteren. Ook kunnen we inschatten hoe lang oudere technologie ondersteund moet worden en wanneer we bijvoorbeeld een nieuwere versie van het besturingssysteem verplicht kunnen stellen zonder dat dit gevolgen heeft voor (te veel) gebruikers. + Verbeter app Anonieme analyse blijft uitgeschakeld %s Bedankt voor uw steun! Register Identificeer uzelf om recepten te downloaden. - Opmerking voor apotheken: De contactgegevens en informatie over apotheken verkrijgen wij van mein-apothekenportal.de van de Duitse Apothekersvereniging Heeft u een fout ontdekt of wilt u gegevens corrigeren? + Opmerking voor apotheken: De contactgegevens en informatie over apotheken verkrijgen wij van mein-apothekenportal.de van de Duitse Apothekenvereniging Heeft u een fout ontdekt of wilt u gegevens corrigeren? Kom meer te weten - apotheken - Dat werkte helaas niet \uD83D\uDE15 + Apotheken + Helaas werkte dat niet \uD83D\uDE15 Probeer het opnieuw. Voer wachtwoord in Verder Toegankelijkheid - zoom - Hiermee kunt u de app vergroten door uw vingers samen te knijpen of te spreiden (pinch-to-zoom). + Zoom + Hiermee kunt u de app vergroten door te knijpen om te zoomen. wachtwoord Beveilig uw gegevens met een wachtwoord naar keuze. wachtwoord - Opslaan op computer - laat wachtwoord zien + Redden + Laat wachtwoord zien herhaal wachtwoord Aanbevelingen: %s - mail schrijven + Email schrijven Wanneer u uw bericht verzendt, wordt de volgende informatie over de gebruikte hardware en het besturingssysteem verzonden: Alleen ter plaatse inwisselen U kunt nog geen e-recepten naar deze apotheek sturen. @@ -173,7 +173,7 @@ Begrepen Herhaalde wachtwoordovereenkomsten Fout 20 10 76631 - Uw gezondheidskaartcertificaat is ongeldig. Is uw kaart verlopen? Neem dan contact op met uw zorgverzekering. + Uw gezondheidskaartcertificaat is ongeldig. Misschien is uw kaart verlopen? Neem dan contact op met uw zorgverzekeraar. Mislukte inlogpogingen Er zijn %s mislukte inlogpogingen gedetecteerd. @@ -182,50 +182,50 @@ Kies de beste apparaatback-up Dit kan een vingerafdruk, veegpatroon of iets dergelijks zijn Munten - toegangstoken + Toegangstokens SSO-tokens Geen toegangstoken beschikbaar geen SSO-token beschikbaar - gekopieerd naar het klembord + naar het klembord gekopieerd Klik om het token naar het klembord te kopiëren Alleen vandaag geldig Toestaan geen verbinding met de server Probeer het over een paar minuten opnieuw - Laad opnieuw - fiches tonen - Hoe wil je de app beveiligen? + Opnieuw laden + Toon tokens + Hoe wilt u de app beveiligen? Kennisgeving Er is geen apparaatback-up ingesteld voor dit apparaat - We raden u aan om uw medische gegevens extra te beschermen met apparaatbeveiliging zoals een toegangscode of biometrie. + We raden u aan uw medische gegevens bovendien te beschermen met apparaatbeveiliging, zoals een code of biometrie. Laat deze melding in de toekomst niet meer zien. Verbinding mislukt. Er kon geen netwerkverbinding tot stand worden gebracht. - Communicatie met de server mislukt: statuscode %s . - Kan niet communiceren met de server: controleer de internetverbinding en de tijd-/datuminstellingen. + Communicatie met server mislukt: statuscode %s . + Communicatie met server mislukt: Controleer de internetverbinding en de tijd-/datuminstellingen. waarschuwing - Uw apparaat is mogelijk minder beveiligd - Dit kan bijvoorbeeld worden veroorzaakt door gemanipuleerde apparaten of een geactiveerde ontwikkelaarsmodus. Om veiligheidsredenen raden we het gebruik van de app op gejailbreakte apparaten af. - Ik erken het verhoogde risico en wil toch doorgaan. + Uw apparaat heeft mogelijk een verminderde beveiliging + Dit kan bijvoorbeeld worden veroorzaakt door gemanipuleerde apparaten of wanneer de ontwikkelaarsmodus is ingeschakeld. Om veiligheidsredenen raden we aan de app niet te gebruiken op gejailbreakte apparaten. + Ik erken het verhoogde risico en wil toch graag doorgaan. Waarom vormen apparaten met root-toegang een potentieel beveiligingsrisico? Kom meer te weten https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html - Profielnaam + Naam van het profiel Voer een naam in voor het nieuwe profiel. - profielnaam - profielen - Hoe een NFC-enabled gezondheidskaart te herkennen + Profielnaam + Profielen + Hoe u een NFC-compatibele zorgkaart kunt herkennen Geen contact mogelijk via deze app - Gebruik de gebruikelijke kanalen om contact op te nemen met uw verzekeringsmaatschappij. - Gezondheidskaart & pincode + Neem via de gebruikelijke kanalen contact op met uw verzekeringsmaatschappij. + Gezondheidskaart en pincode Alleen pincode - Registratie in de e-recepten app + Registratie in de e-receptenapp Het naamveld mag niet leeg zijn. - Er bestaat al een profiel met de ingevoerde naam. + Er bestaat al een profiel met de door u ingevoerde naam. profiel %s geselecteerd Achtergrond kleur - lente grijs - zonnedauw + Lente grijs + Zonnedauw Het! Is! Roze! Boom Blauwe maan september @@ -233,25 +233,25 @@ Samengebonden Laatst verbonden op %s Verwijder profiel? - Hiermee worden alle profielgegevens op dit apparaat gewist. Uw voorschriften in het gezondheidsnetwerk blijven intact. - Blussen - Onderbreken + Hiermee worden alle gegevens uit het profiel op dit apparaat verwijderd. Uw recepten in het zorgnetwerk blijven behouden. + Verwijderen + Annuleren Verwijder profiel U wilt het laatste profiel verwijderen. - De app vereist ten minste één profiel. Voer een naam in voor het nieuwe profiel. + De app vereist minimaal één profiel. Voer een naam in voor het nieuwe profiel. Fout 20 10 76831 - De directory met gezondheidskaarten kon niet worden bereikt. Probeer het opnieuw. - Op het Nationaal Gezondheidsportaal vindt u deskundig geverifieerde informatie over ziekten, ICD-codes en over preventie- en zorgkwesties. - Open Gesund.bund.de - We hebben het privacybeleid gewijzigd - De app voor e-recepten is geëvolueerd. Dit heeft het noodzakelijk gemaakt om ons privacybeleid te actualiseren. + De map met gezondheidskaarten kan niet worden bereikt. Probeer het opnieuw. + Op het Nationaal Gezondheidsportaal vindt u deskundig geverifieerde informatie over ziekten, ICD-codes en preventie- en zorgonderwerpen. + Open healthy.bund.de + Wij hebben het privacybeleid gewijzigd + De e-receptenapp is geëvolueerd. Dit maakte het noodzakelijk om ons privacybeleid te actualiseren. Privacybeleid openen - Dit is veranderd sinds de %s : + Dit is veranderd sinds %s : Wat gebeurt er als je de app opent? Wat gebeurt er als ik de camerafunctie gebruik / recepten lees met de camera? - Geen nieuwe recepten beschikbaar + Er zijn geen nieuwe recepten beschikbaar - %s nieuw recept + %s nieuwe recept %s nieuwe recepten Inwisselbaar @@ -261,38 +261,38 @@ Toegangslogboeken bekijken Wie heeft toegang gekregen tot uw recepten en wanneer? Toegangssleutel tot de receptenservice - toegangslogboeken + Toegang tot logboeken Geen toegangslogboeken Er zijn nog geen toegangslogboeken. - Het recept wordt momenteel verwerkt en kan niet worden verwijderd + Het recept is momenteel in bewerking en kan niet worden verwijderd Aanvaarden - Dat werkte blijkbaar niet - We zijn ons ervan bewust dat de koppeling met de zorgpas valkuilen kent. In de toekomst moet registratie dus ook mogelijk zijn via een reeds geauthenticeerde zorgverzekeringsapp. \n\n We werken er ook aan om recepten digitaal in te wisselen zonder te registreren. \n\n Is u tijdens dit proces iets opgevallen dat u met ons wilt delen? Schrijf ons alstublieft, ook zeer kritische feedback ontvangen wij graag. + Blijkbaar werkte dat niet + Wij zijn ons ervan bewust dat de verbinding met de zorgkaart valkuilen kent. Inschrijven moet in de toekomst ook mogelijk zijn via een reeds geauthenticeerde zorgverzekeringsapp. \n\n Ook werken we eraan dat recepten digitaal kunnen worden ingewisseld zonder registratie. \n\n Is u tijdens dit proces iets opgevallen dat u graag met ons wilt delen? Schrijf ons gerust, wij ontvangen ook graag zeer kritische feedback. Verbindingstips Verbeter de sterkte van de verbinding - Verwijder indien nodig de beschermkap. - Als het apparaat trilt en vervolgens de verbinding verbreekt, zoek dan naar de optimale positie binnen een kleine straal. + Verwijder indien nodig de beschermhoes. + Als het apparaat trilt en de verbinding vervolgens wordt verbroken, zoek dan binnen een kleine straal naar de optimale positie. Beweeg het apparaat heel langzaam over de kaart. - Plaats het apparaat direct op de kaart. - Leg hiervoor de gezondheidskaart op een vlakke ondergrond (bijvoorbeeld een tafel). + Plaats het apparaat rechtstreeks op de kaart. + Plaats hiervoor de gezondheidskaart op een vlakke ondergrond (bijvoorbeeld een tafel). Verbeter de sterkte van de verbinding Let op de plaatsing van de NFC-sensor - Zoek uit waar de NFC-sensor zich in uw apparaat bevindt (hier bijvoorbeeld een overzicht voor apparaten van %s ). + Ontdek waar de NFC-sensor zich in uw apparaat bevindt (hier bijvoorbeeld een overzicht voor apparaten van %s ). In sommige gevallen kan de positie van de NFC-sensor binnen een modelserie verschillen (hier bijvoorbeeld de informatie voor de %s ). Volgende tip Verder Dichtbij - Probeer + Poging Schrijf ons - Zoeklicentie voor apotheek - inwisselen - Gescand voorschrift + Licentie Apotheek Zoeken + Inwisselen + Gescand recept Gescand op %s Gemarkeerd als ingewisseld op %s Hoe wil je verder? Volgorde Binnenkort beschikbaar - Reserveer nu om af te halen of te laten bezorgen per koerier of verzending + Reserveer nu voor afhaling of laat het bezorgen via koerier of verzending Bewaar voor latere bestelling Bewaar recepten op het apparaat @@ -300,42 +300,42 @@ Ga verder met %s recepten Verbinding met gezondheidskaart is mislukt - Het huidige profiel is al gekoppeld aan een andere zorgkaart (ziekteverzekeringsnummer %s ). - Je gezondheidskaart is al gekoppeld aan een ander profiel. Schakel over naar profiel %s . - Opslaan op computer - contactgegevens en adres + Het huidige profiel is al gekoppeld aan een andere zorgkaart (zorgverzekeringsnummer %s ). + Uw zorgkaart is al gekoppeld aan een ander profiel. Ga naar profiel %s . + Redden + Contactgegevens en adres Contact - telefoonnummer - Geef een telefoonnummer op voor contact. + Telefoon nummer + Geef een telefoonnummer op om contact met ons op te nemen. E-mailadres (optioneel) bezorgadres - voornaam en achternaam - Vul a.u.b. een voor- en achternaam in voor contactdoeleinden. + Voornaam en achternaam + Geef een voor- en achternaam op om contact met ons op te nemen. straat en huisnummer - Vul a.u.b. een straatnaam en huisnummer in zodat wij gecontacteerd kunnen worden. + Geef een straat en huisnummer op om contact met ons op te nemen. Extra adres (optioneel) - Bezorginstructie (optioneel) - Aanvullende contactgegevens vereist + Leveringsinstructies (optioneel) + Verdere contactgegevens vereist Veranderingen ongedaan maken? weggooien - Voor het zoeken gebruikt de apotheekdirectory geografische coördinaten die zijn bepaald met behulp van OpenStreetMap. Wij danken het project voor deze hulp. - © OpenStreetMap ( %s ) + Voor het zoeken maakt de apotheekgids gebruik van geocoördinaten die zijn bepaald met behulp van OpenStreetMap. Wij danken het project voor deze hulp. + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Privacy & Gebruik + Gegevensbescherming en gebruik Verder U heeft uw pincode ontvangen in een brief van uw zorgverzekeraar. Geen pincode ontvangen pincode - Controleer de internetverbinding en de tijd-/datuminstelling van uw apparaat. + Controleer de internetverbinding en de tijd-/datuminstellingen van uw apparaat. Om in te loggen, drukt u op “Ontgrendelen”. - buitengesloten? Verifieer uw biometrische referenties op dit apparaat. - Wachtwoord vergeten? Verwijder de app en installeer deze opnieuw. U kunt ontdekken waarom in onze %s . + Buitengesloten? Controleer uw biometrische gegevens op dit apparaat. + Wachtwoord vergeten? Verwijder de app en installeer deze vervolgens opnieuw. Je kunt in onze %s ontdekken waarom. hulp gebied - verpakkingsgrootte en eenheid + Verpakkingsgrootte en eenheid actief ingrediënt Hoeveelheid actief ingrediënt - batch aanduiding - Exp + Batchnaam + Uitv categorie Vaccin Aanvaarden @@ -350,145 +350,145 @@ Wachtwoord is niet zichtbaar biometrie wachtwoord - wachten op antwoord + Wachten op antwoord Geen recepten U heeft momenteel geen inwisselbare recepten. Updaten Automatisch uitloggen - Om veiligheidsredenen wordt de verbinding met de receptenserver na 12 uur verbroken. Maak opnieuw verbinding om actuele recepten te krijgen. + Om veiligheidsredenen wordt de verbinding met de receptserver na 12 uur verbroken. Maak opnieuw verbinding om de huidige recepten te krijgen. Aansluiten - Heeft u een papieren exemplaar ontvangen? + Heeft u een papieren afdruk ontvangen? Voeg recepten toe aan uw lijst door op de scanknop in de rechterbovenhoek te tikken. - Scan de papieren afdruk - Je moet ingelogd zijn om recepten automatisch te ontvangen. + Papierafdruk scannen + Om recepten automatisch te ontvangen, moet u ingelogd zijn. Register Geen ingewisselde recepten - Uw ingewisselde recepten worden hier weergegeven. Om redenen van gegevensbescherming worden uw recepten na 100 dagen van de receptenserver verwijderd. + Hier worden uw ingewisselde recepten weergegeven. Om redenen van gegevensbescherming worden uw recepten na 100 dagen van de receptenserver verwijderd. Geen ingewisselde recepten - Uw ingewisselde recepten worden hier weergegeven. Voeg recepten toe via scan om te beginnen met inwisselen. - apparaatbeheer + Hier worden uw ingewisselde recepten weergegeven. Voeg recepten toe via scan om te beginnen met inwisselen. + Apparaatbeheer Verbonden apparaten Geregistreerd sinds %s (dit apparaat) Geregistreerd sinds %s - Om veiligheidsredenen wordt de verbinding met de receptenserver na 12 uur verbroken. Om opnieuw verbinding te maken, hebt u voor elk verbindingsproces een gezondheidskaart en een pincode nodig. + Om veiligheidsredenen wordt de verbinding met de receptserver na 12 uur verbroken. Om opnieuw verbinding te maken, heeft u voor elk verbindingsproces een gezondheidskaart en een pincode nodig. pincode - Voer uw pincode (gezondheidskaart) in. + Voer pincode in (gezondheidskaart). Verder Register Verbonden apparaten Verwijder apparaat? - Onderbreken - VERWIJDERD + Annuleren + Verwijderen Dit apparaat verwijderen? Wilt u %s verwijderen? - Als u %s verwijdert, wordt de verbinding met de receptenserver uiterlijk binnen 12 uur definitief verbroken. - Apparaten worden geladen... + Als u %s verwijdert, wordt de verbinding met de receptserver uiterlijk binnen 12 uur definitief verbroken. + Apparaten worden geladen… Geen apparaten - Er zijn geen toestellen aangesloten op deze gezondheidskaart. + Er zijn geen apparaten aangesloten op deze zorgkaart. Probeer het nog eens Oh Oh :-( - Apparatenlijst kon niet worden geladen. + Apparaatlijst kan niet worden geladen. wwweg… Geen internet verbinding. Medicijnen en verbandmiddelen - narcotica - Levering van geneesmiddelen op recept volgens § 4 AMVV + Narcotica + Verstrekking van receptgeneesmiddelen conform artikel 4 AMVV Heb je hulp nodig? - We hebben enkele tips voor je op een rij gezet om de meest voorkomende problemen op te lossen. - Begin met verbindingstips - ontgrendelen + Wij hebben enkele tips voor u op een rij gezet om de meest voorkomende problemen op te lossen. + Start verbindingstips + Ontgrendelen kaart geblokkeerd De pincode is drie keer verkeerd ingevoerd. Uw kaart is daarom om veiligheidsredenen geblokkeerd. - kaart ontgrendelen + Ontgrendel kaart Voer PUK in - Met uw pincode heeft u van uw verzekeringsmaatschappij een 8-cijferige PUK ontvangen. + Met uw pincode heeft u van uw verzekeringsmaatschappij een 8-cijferige PUK-code ontvangen. Kies een nieuwe pincode - U kunt zelf uw nieuwe persoonlijk identificatienummer (PIN) kiezen (6 tot 8 cijfers). - Pincode onthouden? + Je nieuwe persoonlijke identificatienummer (PIN) kun je zelf kiezen (6 tot 8 cijfers). + Bent u uw pincode vergeten? Noteer uw pincode en bewaar deze op een veilige plaats. - Onderbreken + Annuleren OK Ontgrendelen niet mogelijk - U heeft het maximale aantal kaartontgrendelingen met deze puk bereikt of heeft deze herhaaldelijk verkeerd ingevoerd. Neem dan contact op met uw verzekeringsmaatschappij. + U heeft het maximale aantal kaartontgrendelingen met deze PUK bereikt of heeft de PUK herhaaldelijk verkeerd ingevoerd. Neem contact op met uw verzekeringsmaatschappij. U kunt één PUK gebruiken voor maximaal 10 ontgrendelingen. - kaart ontgrendeld + Kaart ontgrendeld Wat je nodig hebt: - uw gezondheidskaart - PUK van uw zorgpas + Uw gezondheidskaart + PUK van uw zorgkaart Verder - gezondheidskaart - Pincode of kaart bestellen + Gezondheidskaart + Bestel pincode of kaart Register Hoe wil je inloggen? - NFC-enabled gezondheidskaart - Pincode voor de zorgpas - Heb je nog geen NFC-compatibele gezondheidskaart en pincode? + NFC-compatibele gezondheidskaart + Pincode voor de zorgkaart + Heeft u nog geen NFC-compatibele zorgkaart en pincode? Nu toepassen - Of: Log in met de %s . - Uw zorgverzekering app - "Uw toegangsnummer vindt u rechtsboven op uw gezondheidskaart." + Of: Log in met %s . + Uw zorgverzekeringsapp + "U vindt uw toegangsnummer rechtsboven op uw zorgkaart." Mijn kaart heeft geen toegangsnummer - Je hebt nog %s pogingen voordat je kaart wordt geblokkeerd. - Je hebt nog %s pogingen voordat je kaart wordt geblokkeerd. + U heeft %s nog één poging voordat uw kaart wordt geblokkeerd. + Je hebt nog %s meer pogingen voordat je kaart wordt geblokkeerd. Plaats de gezondheidskaart op de achterkant van de telefoon - Het volgende proces kan tot 30 seconden duren. + Het volgende proces kan maximaal 30 seconden duren. Plaats kaart %s op de achterkant van de telefoon. - in de rechterbovenhoek - in het bovenste midden - linksboven + in het gebied rechtsboven + in het midden in het bovenste gedeelte + in het gebied linksboven in het middengebied aan de rechterkant midden - in het centrum links + in het middengebied aan de linkerkant in het gebied rechtsonder - in het onderste midden - linksonder + in het midden in het onderste gedeelte + in het gebied linksonder Hulp %s minuten geleden verzonden Verzonden op %s Zojuist verzonden - Verzonden om %s uur + Verzonden om %s tijd Niet meer geldig - Log in met de app - verzekering kiezen - Niet gevonden wat u zocht? Deze lijst wordt voortdurend uitgebreid. Registratie met een zorgpas wordt al door elke zorgverzekeraar ondersteund. - Feedback van de e-recepten-app - We kijken uit naar uw feedback. Gebruik de ruimte hieronder en wees zo nauwkeurig mogelijk: + Registreren met app + Kies een verzekering + Niet gevonden wat je zocht? Deze lijst wordt voortdurend uitgebreid. Registratie met een zorgkaart wordt al door elke zorgverzekeraar ondersteund. + Feedback van de e-receptenapp + Wij kijken uit naar uw feedback. Gebruik de volgende ruimte en wees zo nauwkeurig mogelijk: PUK Dichtbij Wat jammer… - Helaas voldoet uw apparaat niet aan de minimale vereisten om in te loggen op de e-recepten-app. Voor veilige authenticatie met je zorgpas zijn minimaal Android 7 en een NFC-chip nodig. + Helaas voldoet uw apparaat niet aan de minimale vereisten voor registratie in de e-receptenapp. Voor veilige authenticatie met uw zorgkaart zijn minimaal Android 7 en een NFC-chip vereist. Kom meer te weten - Inloggegevens bewaren? - Opslaan op computer - Sla niet op + Inloggegevens opslaan? + Redden + Bewaar niet Kennisgeving - Om veiligheidsredenen wordt de verbinding met de receptenserver na 12 uur verbroken. Om opnieuw verbinding te maken, hebt u voor elk verbindingsproces uw gezondheidskaart en pincode nodig. - Stel biometrische beveiliging in - Het opslaan van toegangsgegevens is niet mogelijk. Stel vooraf biometrische beveiliging (bijv. vingerafdruk) op uw apparaat in. - Onderbreken - Ideeën + Om veiligheidsredenen wordt de verbinding met de receptserver na 12 uur verbroken. Om opnieuw verbinding te maken, heeft u voor elk verbindingsproces een gezondheidskaart en een pincode nodig. + Biometrische beveiliging instellen + Het is niet mogelijk om toegangsgegevens op te slaan. Stel vooraf biometrische beveiliging (bijvoorbeeld vingerafdruk) in op uw apparaat. + Annuleren + Instellingen Kennisgeving Aanvaarden Beveiliging van uw receptgegevens - \"Deze app gebruikt de veiligste biometrische sensor van uw apparaat om uw inloggegevens op te slaan in een beveiligd gedeelte van het apparaatgeheugen.\" - Door de biometrische beveiliging van uw toegangsgegevens kunt u deze app in de toekomst zonder invoer van uw pincode en gezondheidskaart openen en recepten inzien, opvragen, inwisselen of verwijderen. - Zorg ervoor dat personen met wie u dit apparaat mogelijk deelt en van wie de biometrische kenmerken op dit apparaat kunnen worden opgeslagen, ook toegang hebben tot uw recepten. - dat is helaas niet gelukt - Verificatie met de zorgverzekeringsapp is niet gelukt. + \"Deze app maakt gebruik van de veiligste biometrische sensor die door uw apparaat wordt geleverd om uw inloggegevens te beveiligen in een beschermd gebied van de apparaatopslag.\" + Door de biometrische beveiliging van uw toegangsgegevens kunt u deze app in de toekomst openen, recepten bekijken, opvragen, inwisselen of verwijderen zonder zorgpas en het invoeren van uw pincode. + Zorg ervoor dat mensen met wie u dit apparaat deelt en wier biometrische kenmerken mogelijk op dit apparaat zijn opgeslagen, ook toegang hebben tot uw recepten. + dat werkte helaas niet + Authenticatie met de zorgverzekeringsapp is niet gelukt. Verlopen op %s - Het recept is al verwijderd van de server - Corrigeer uw invoer of gooi wijzigingen weg + Het recept is al van de server verwijderd + Corrigeer uw invoer of annuleer de wijzigingen Juist - verzekerde gegevens + Gegevens van verzekerde personen Achternaam Verzekering - verzekerings nummer - kaart toegangsnummer + Verzekerings nummer + Kaarttoegangsnummer Register - Afmelden - Opslaan op computer + Uitloggen + Redden Wijziging Profielfoto wijzigen Verder @@ -496,16 +496,16 @@ Probeer het later opnieuw. Probeer het nog eens Zoek naar verzekeringen - Nu verbinding maken met de receptenserver? + Nu verbinding maken met de receptserver? succesvol ingelogd verbinding verbroken - Nu verbinding maken met de receptenserver? - Geen penningen - U ontvangt een token wanneer u bent ingelogd op de receptenservice.\n - bestellingen + Nu verbinding maken met de receptserver? + Geen tokens + U ontvangt een token wanneer u bent ingelogd bij de receptenservice.\n + Bestellingen Selecteer de gewenste pincode - kaart ontgrendelen - Kies pincode + Ontgrendel kaart + Selecteer Pincode Herhaal pincode De inzendingen verschillen van elkaar. Geen bestellingen @@ -513,67 +513,67 @@ Net nu Om %s uur Winkelwagen is klaar - Het recept is toegevoegd aan je winkelmandje. Ga naar de website van de apotheek om de bestelling af te ronden. + Het recept is toegevoegd aan uw winkelwagen. Ga naar de website van de apotheek om de bestelling af te ronden. Winkelwagen openen Toon deze afhaalcode bij de apotheek. Ophaalcode ontvangen Bericht kan niet worden getoond Neem contact op met uw apotheek ( %s ). - Winkelwagenlink tonen + Toon winkelwagenlink Toon afhaalcode Laat het bericht zien %s om %s uur Recept verzonden naar %s . - Bestel overzicht + Besteloverzicht Nieuw Cursus - Volgorde - Gratis voor de beller. Servicetijden: ma - vr 08:00 - 20:00 behalve op nationale feestdagen + De bestelling + Gratis voor de beller. Servicetijden: ma t/m vr 8.00 - 20.00 uur behalve op nationale feestdagen Apotheek Selecteer de gewenste pincode Gewenste pincode opgeslagen Momenteel open en bij mij in de buurt Filteren op … start met zoeken - directe opdracht - apotheken - telefoon nummer (optioneel) + Directe opdracht + Apotheken + Telefoonnummer (optioneel) Zoek op naam of adres Geen geldige apotheekinformatie - Er is geen actuele informatie gevonden over deze apotheek. De invoer voor deze apotheek wordt verwijderd. + Er zijn geen actuele gegevens gevonden over deze apotheek. De vermelding voor deze apotheek wordt verwijderd. OK - Apotheeklijst niet beschikbaar - Momenteel kan er geen actuele informatie over deze apotheek worden opgevraagd. Controleer uw internetverbinding. - Onderbreken + Apotheekgids niet beschikbaar + Momenteel is er geen actuele informatie over deze apotheek beschikbaar. Controleer uw internetverbinding. + Annuleren Probeer het nog eens - Milieu redden + Milieu besparen Inloggen is niet mogelijk - Het lijkt erop dat uw biometrische inloggegevens zijn gewijzigd. Gelieve opnieuw te registreren met uw gezondheidskaart. - Onderbreken + Het lijkt erop dat uw biometrische inlogkenmerken zijn gewijzigd. Log opnieuw in met uw zorgpas. + Annuleren Register - profiel 1 + Profiel 1 Dicht bij mij Later inwisselbaar Inwisselbaar vanaf %s - productverbeteringen + Productverbeteringen Anonieme analyse - Help ons deze app beter te maken. Alle gebruiksgegevens worden anoniem verzameld en worden alleen gebruikt om de gebruikerservaring te verbeteren. - apparaat beveiliging + Help ons deze app beter te maken. Alle gebruiksgegevens worden anoniem verzameld en uitsluitend gebruikt om de gebruikerservaring te verbeteren. + Apparaatbeveiliging persoonlijke instellingen Toegankelijkheid - productverbeteringen + Productverbeteringen Recept toegevoegd Recept al beschikbaar Er is een fout opgetreden tijdens het importeren - Blussen - Gescand voorschrift - Vervanging mogelijk + Verwijderen + Gescand recept + Vervangingsvoorbereiding mogelijk Pincode vergeten - %s recept + %s Recept %s Recepten - Ik heb het privacybeleid en de gebruiksvoorwaarden gelezen en geaccepteerd. + Ik heb het privacybeleid en de gebruiksvoorwaarden gelezen en ga ermee akkoord. Gegevensbescherming Gebruiksvoorwaarden We zouden graag: @@ -583,143 +583,142 @@ U kunt deze beslissing op elk moment wijzigen in de systeeminstellingen. Doorgaan Aanvaarden - Deze app gebruikt de veiligste methode van uw apparaat. - Opslaan op computer + Deze app gebruikt de veiligste methode die door uw apparaat wordt geboden. + Redden Kiezen medicijn - handelsnaam + Handelsnaam Ja Nee dosering Uitgavedatum Dit recept wordt voor u ingewisseld als onderdeel van een behandeling. Niet gespecificeerd - extra betaling + Extra betaling medicijn - Pakbonnen - Komt in aanmerking volgens BVG - alternatieve bereiding - recept naam + Instructies voor indiening + Geschikt volgens BVG + Alternatieve bereiding + Receptnaam Verpakking - knutselinstructie + Productie-instructies Beschrijving gegeven door uitgegeven op: actief ingrediënt - voorgeschreven + Voorgeschreven Ontvangen Wat is een directe opdracht? - Bij directe verwijzingen wordt een recept uit een praktijk of ziekenhuis direct bij een apotheek ingewisseld. Verzekerden hoeven niets te doen en kunnen niet tussenkomen in het afkoopproces. \n\n Directe verwijzingen worden vermeld in de e-prescription-app om uw behandeling voor u transparanter te maken. - toeslag voor noodhulp - Soms is haast geboden. Sommige recepten kunnen worden ingewisseld zonder extra betaling van een spoedservice, zoals \'s nachts of op feestdagen. - Geneesmiddelen met eigen bijdrage - Vrijgesteld van eigen bijdrage - Degenen met een wettelijke ziektekostenverzekering moeten een eigen bijdrage betalen van maximaal tien euro voor geneesmiddelen op recept. \n\n De hoogte van het eigen risico is afhankelijk van de prijs van uw medicatie. Geneesmiddelen die minder dan € 5 kosten, moet u zelf betalen.\n Voor medicijnen die duurder zijn, moet u tien procent van de prijs betalen, maar minimaal € 5 en maximaal € 10. \n\n Kinderen en jongeren onder de 18 jaar zijn over het algemeen vrijgesteld van eigen bijdrage. \n\n Als uw jaarlijkse kosten voor geneesmiddelen uw financiële grens overschrijden, kunt u worden vrijgesteld van de eigen bijdrage. Overleg hierover met uw zorgverzekeraar. - U bent vrijgesteld van de eigen bijdrage van dit geneesmiddel. Uw zorgverzekering vergoedt de medicatie. + Bij directe verwijzing wordt een recept uit een praktijk of ziekenhuis rechtstreeks bij een apotheek afgevuld. Verzekerde personen hoeven geen actie te ondernemen en kunnen niet tussenkomen in het terugbetalingsproces. \n\n Directe verwijzingen staan ​​vermeld in de e-receptenapp om uw behandeling voor u transparanter te maken. + Kosten voor noodservice + Soms is haast noodzakelijk. Sommige recepten kunnen worden ingevuld zonder extra kosten voor de spoedservice, bijvoorbeeld \'s nachts of op feestdagen. + Medicijnen tegen eigen bijdrage + Vrijgesteld van bijbetaling + Degenen met een wettelijke ziektekostenverzekering moeten een extra vergoeding van maximaal tien euro betalen voor voorgeschreven medicijnen. \n\n De hoogte van de bijbetaling is afhankelijk van de prijs van uw medicijnen. Geneesmiddelen die minder dan € 5,- kosten, moet u zelf betalen.\n Voor medicijnen die duurder zijn, moet je tien procent van de prijs betalen, maar minimaal € 5 en maximaal € 10. \n\n Kinderen en jongeren onder de 18 jaar zijn over het algemeen vrijgesteld van bijbetaling. \n\n Als uw jaarkosten voor medicijnen hoger zijn dan uw financiële lastengrens, kunt u worden vrijgesteld van de eigen bijdrage. Praat hierover met uw zorgverzekeraar. + U bent vrijgesteld van het betalen van een eigen bijdrage voor dit geneesmiddel. Uw zorgverzekeraar vergoedt de kosten van de medicijnen. Hoe lang is dit recept geldig? Tijdens deze periode kunt u uw recept in elke apotheek inwisselen tegen een maximale bijbetaling van € 10,-. - Vervanging mogelijk - Vanwege de wettelijke vereisten van uw zorgverzekeraar kunt u een alternatief krijgen met dezelfde werkzame stof. \n\n Geneesmiddelen kunnen er anders uitzien en anders worden genoemd, hebben verschillende prijzen en fabrikanten, maar bevatten toch dezelfde werkzame stof. Vooral de werkzame stof zelf en de dosering zijn belangrijk voor de werking van medicijnen in het lichaam. Patiënten in de apotheek krijgen vaak een ander middel dan de arts op het recept heeft voorgeschreven - mits de middelen vergelijkbaar zijn. Er kunnen therapeutische en economische redenen zijn voor de verandering. - Gescand voorschrift - Om veiligheidsredenen mogen recepten die zijn geïmporteerd uit een papieren afdruk geen persoonlijke of medische gegevens bevatten. \n\n Log in op deze app met de zorgkaart of verzekeringsapp om alle informatie op het recept te bekijken. - Recept klopt niet - Dit voorschrift is ten onrechte afgegeven. - Gescand voorschrift - toeslag voor spoedeisende hulp + Vervangingsvoorbereiding mogelijk + Vanwege wettelijke eisen van uw zorgverzekeraar kan het zijn dat u een alternatief krijgt met dezelfde werkzame stof. \n\n Geneesmiddelen kunnen er anders uitzien en anders heten, verschillende prijzen en fabrikanten hebben, maar toch dezelfde werkzame stof bevatten. De werkzame stof zelf en de dosering zijn cruciaal voor de werking van medicijnen in het lichaam. Vaak krijgen patiënten bij de apotheek een ander medicijn dan de arts heeft voorgeschreven, mits de medicijnen vergelijkbaar zijn. Er kunnen therapeutische en economische redenen zijn voor de verandering. + Gescand recept + Recepten die van een papieren versie zijn geïmporteerd, kunnen om veiligheidsredenen geen persoonlijke of medische informatie weergeven. \n\n Log in op deze app met zorgpas of verzekeringsapp om alle informatie op het recept te bekijken. + Recept onjuist + Dit recept is ten onrechte verstrekt. + Kosten voor noodservice Dosering volgens schriftelijke instructies telefoon - plaats + website Mail - Sorteren op afstand is niet mogelijk. + Sorteren op afstand niet mogelijk. OK Voer de huidige pincode in - Onjuiste pincode ingevoerd - De huidige pincode van uw zorgpas + Verkeerde pincode ingevoerd + De huidige pincode van uw zorgkaart kaart geblokkeerd - Deblokkeer je kaart in Instellingen > Kaart deblokkeren. - Voer om veiligheidsredenen uw huidige pincode in. + Ontgrendel uw kaart via Instellingen > Kaart ontgrendelen. + Om veiligheidsredenen dient u uw huidige pincode in te voeren. Pincode vergeten - Onjuist recept + Verkeerd recept medicijn - Er lijkt iets mis te zijn gegaan tijdens het maken van je recept. Fout melden? + Er lijkt iets mis te zijn gegaan bij het maken van uw recept. Een fout melden? Rapport Niet ingelogd Aangemeld met - gezondheidskaart + Gezondheidskaart biometrie Niet ingelogd - Wij zijn geïnteresseerd in uw mening. Neem vijf minuten de tijd om onze enquête in te vullen. Alvast bedankt. - waarschuwingsbericht + Wij zijn geïnteresseerd in uw mening. Neem vijf minuten de tijd om onze enquête in te vullen. Alvast heel erg bedankt. + Waarschuwingsbericht Apotheek toegevoegd aan favorieten Apotheek verwijderd uit favorieten Mijn apotheken Wachtwoordsterkte zeer goed - Schrijfbewerking mislukt + Schrijfbewerking niet succesvol Pincode kan niet worden opgeslagen Rapport Pincode toewijzen Toegangsregel geschonden - U heeft geen toestemming om toegang te krijgen tot de kaartmap. + U heeft geen toestemming voor toegang tot de kaartmap. Wijs uw eigen pincode toe - De kaart is beveiligd met een pincode van uw zorgverzekeraar (transportpincode), wijs uw eigen pincode toe. + De kaart is beveiligd met een pincode van uw zorgverzekeraar (vervoerpincode), voer uw eigen pincode in. Wachtwoord niet gevonden Er is geen wachtwoord op uw kaart opgeslagen. je bent uitgelogd Log opnieuw in om uw recepten bij te werken. - actief ingrediënt nummer + Actief ingrediëntnummer kracht en eenheid %s minuten geleden ingewisseld - ingewisseld op %s + Ingewisseld op %s Zojuist verzilverd Ingewisseld om %s uur Bestellingen - Dit recept is voor u ingewisseld als onderdeel van een behandeling. - toeslag voor noodhulp - Dit recept kan niet \'s nachts in een apotheek worden ingevuld zonder extra betaling van een spoedservicetarief. + Dit recept is ingevuld als onderdeel van een behandeling voor u. + Kosten voor noodservice + Dit recept kan niet \'s nachts in een apotheek worden afgevuld zonder extra betaling van een spoedservicetoeslag. Zoek hier - Ideeën - Locatie delen in instellingen. + Instellingen + Deel locatie in Instellingen. Dicht bij mij Houd ingedrukt om de naam te bewerken. Voer de nieuwe naam voor het profiel in. - U moet ingelogd zijn om digitale recepten van uw praktijk te ontvangen. + Om recepten digitaal te ontvangen vanuit uw praktijk, moet u ingelogd zijn. Recepten digitaal ontvangen? - Sleep het scherm omlaag om te vernieuwen. + Trek het scherm omlaag om te vernieuwen. Geen recepten Voeg recepten toe met de + knop in de rechterbovenhoek. Register - receptenarchief + Receptarchief Misschien later Register Profielfoto wijzigen - receptenarchief + Receptarchief Voer naam in - Opslaan op computer + Redden Mijn bestelling - Ontvanger: in - recepten + Ontvanger: binnen + Recepten Apotheek Versturen Wijziging - Afhalen bij de apotheek + Ophalen bij de apotheek Levering per koerier - Levering per post + Levering per postorder %s Recepten - Inwisselen niet mogelijk - Een of meer recepten konden niet worden ingewisseld. - Geen recept geselecteerd - Om recepten in te wisselen, moet er ten minste één recept zijn geselecteerd. - Contactgegevens toevoegen + Niet mogelijk om te verzilveren + Eén of meer recepten konden niet worden ingewisseld. + Geen recept gekozen + Om recepten in te wisselen, moet er minimaal één recept worden geselecteerd. + Voeg contactgegevens toe Wijziging Geen recept U heeft momenteel geen inwisselbare recepten verzameling - koerier + bezorger Verzending - kies recepten + Kies recepten Tik hier om recepten te scannen Lang indrukken om namen te bewerken - Voeg meer profielen toe, bijvoorbeeld voor uw kinderen of ouders + Voeg extra profielen toe, bijvoorbeeld voor uw kinderen of ouders Klik op het scherm om de weergegeven tooltip over te slaan. Hoe inwisselen? Hoe wilt u uw medicatie ontvangen? @@ -728,10 +727,10 @@ Volgorde Reserveer of laat het bezorgen Klaar - collectieve code - enkele codes + Ophaalcode + Individuele codes - U heeft een recept van %s . + Je hebt %s recept. Je hebt %s recepten. Maak een selectie @@ -741,67 +740,67 @@ Verder Kom meer te weten Kennisgeving - Deze app gebruikt software van Google om codes te herkennen. + Deze app maakt gebruik van software van Google om codes te herkennen. Kom meer te weten - Over de receptcodescanner + Informatie over de receptcodescanner Welke gegevens bevat de receptcode? - De receptcode bevat alleen een identificatie van het recept. Hierdoor is het recept terug te vinden op de receptenservice in het digitale gezondheidsnetwerk. De receptcode bevat geen gegevens over u of uw medicatie. - Dus niemand kan iets met de receptcode alleen? - Juist. De receptgegevens moeten worden gedownload van de receptenservice. Hiervoor is een beveiligde login vereist. - Wie kan zich inschrijven voor de receptenservice? - Inschrijven bij de receptenservice in het digitale gezondheidsnetwerk is mogelijk voor verzekerden, apotheken, artsenpraktijken en ziekenhuizen. - Waarom gebruikt de e-prescription-app Google-functies? - Google biedt functies die eenvoudig in apps kunnen worden ingebouwd en die voortdurend door Google worden ontwikkeld en bijgewerkt. Dit zorgt ervoor dat de functies op veel verschillende eindapparaten werken en veilig kunnen worden bediend. De app gebruikt een functie om de camera- en scanfunctionaliteit voor Android-apparaten te verbeteren (Google ML Kit). - Hoe werkt Google ML Kit-scanverbetering? + De receptcode bevat alleen een identificatie voor het recept. Dit betekent dat het recept te vinden is op de receptenservice in het digitale zorgnetwerk. De receptcode bevat geen informatie over u of uw medicatie. + Dus niemand kan iets doen met alleen de receptcode? + Juist. De receptgegevens moeten worden gedownload van de receptenservice. Hiervoor is een beveiligde login nodig. + Wie kan zich aanmelden voor de receptenservice? + Registratie voor de receptenservice in het digitale zorgnetwerk is mogelijk voor verzekerden, apotheken, praktijken en ziekenhuizen. + Waarom gebruikt de e-receptenapp Google-functies? + Google biedt functies die eenvoudig in apps kunnen worden geïntegreerd en die Google voortdurend ontwikkelt en updatet. Dit zorgt ervoor dat de functies op veel verschillende apparaten werken en veilig kunnen worden bediend. De app maakt gebruik van een functie om de camera- en scanfunctionaliteit voor Android-apparaten te verbeteren (Google ML Kit). + Hoe werkt scanverbetering met Google ML Kit? Google ML Kit helpt het door een camera vastgelegde beeld te optimaliseren, zodat de receptcodes zelfs bij slechte lichtomstandigheden of met oudere cameramodellen kunnen worden gelezen. - Worden gegevens over het voorschrift of mijn medicatie doorgegeven aan Google? - Nee. De gelezen receptcode wordt direct in de app opgeslagen. Het wordt niet doorgegeven aan Google. De receptgegevens worden niet in de code opgeslagen, alleen in het digitale gezondheidsnetwerk. Van daaruit worden ze naar de app gestuurd. Google heeft geen toegang tot het digitale gezondheidsnetwerk. + Worden gegevens over het recept of mijn medicatie gedeeld met Google? + Nee. De gelezen receptcode wordt direct in de app opgeslagen. Het wordt niet gedeeld met Google. De receptgegevens worden niet in de code opgeslagen, maar alleen in het digitale zorgnetwerk. Van daaruit worden ze naar de app verzonden. Google heeft geen toegang tot het digitale gezondheidsnetwerk. Welke gegevens verwerkt Google bij het gebruik van ML Kit? - Google heeft alleen toegang tot technische informatie over het gebruikte eindapparaat en het algemene gebruik van de extra functie (bijv. foutpercentage, camera-instellingen) om dit statistisch vast te leggen en zo de extra functie te verbeteren. Bij uw toegang registreert Google tijdelijk het IP-adres van uw eindapparaat. Informatie over u en de inhoud van het recept wordt niet door Google vastgelegd. + Google krijgt alleen toegang tot technische informatie over het gebruikte apparaat en het algemene gebruik van de extra functie (bijv. foutpercentage, camera-instellingen) om dit statistisch vast te leggen en zo de extra functie te verbeteren. Bij toegang registreert Google tijdelijk het IP-adres van uw apparaat. Gegevens over u en de inhoud van het recept worden niet door Google vastgelegd. Is het gebruik van Google ML Kit vrijwillig? - Ja. ML Kit is echter ingebouwd in de receptcodescanner in de Android-versie van de e-prescription-app. Als u de receptcodescanner op een Android-apparaat gebruikt, wordt ook altijd de ML Kit-functie gebruikt. U kunt het echter zonder de receptcodescanner doen. Uw recepten kunnen ook in de app worden geladen als u zich met de elektronische gezondheidskaart of via de app van uw zorgverzekering aanmeldt bij het digitale zorgnetwerk. + Ja. ML Kit is echter ingebouwd in de receptcodescanner in de Android-versie van de e-receptenapp. Als u de receptcodescanner op een Android-apparaat gebruikt, wordt altijd de ML Kit-functie gebruikt. U kunt echter het gebruik van de receptcodescanner vermijden. Uw recepten kunnen ook in de app worden geladen als u met de elektronische zorgkaart of via uw zorgverzekeringsapp inlogt op het digitale zorgnetwerk. Kan ik zien wie mijn recepten heeft bekeken? - Ja. Alle toegang tot uw gegevens wordt volledig ingelogd in het digitale gezondheidsnetwerk. In de e-recepten-app kunt u zien wie er toegang heeft tot uw gegevens. - Met wie kan ik contact opnemen als ik vragen heb over de app of het e-recept? + Ja. Alle toegang tot uw gegevens wordt volledig geregistreerd in het digitale zorgnetwerk. In de e-receptenapp kunt u zien wie toegang heeft gehad tot uw gegevens. + Waar kan ik terecht als ik vragen heb over de app of het e-recept? Gedetailleerde informatie vindt u in de gegevensbeschermingsverklaring. Aantal voorgeschreven verpakkingen Geen recepten - Hiervoor heeft u verzilverbare recepten nodig. - verzekering kiezen + Hiervoor heeft u inwisselbare recepten nodig. + Kies een verzekering Zoek naar verzekeringen - Onderbreken + Annuleren Wat wilt u aanvragen? - Voor deze app heb je een pas en de bijbehorende pincode nodig. + Voor deze app heb je een kaart en de bijbehorende pincode nodig. Hoe wilt u contact opnemen met uw verzekeringsmaatschappij? Uw verzekeringsmaatschappij biedt de volgende contactmogelijkheden - Uw verzekeringsmaatschappij biedt de volgende contactmogelijkheden + Uw verzekeringsmaatschappij biedt de volgende contactmogelijkheid Dichtbij - Pincode onjuist ingevoerd. + Pincode verkeerd ingevoerd. Toegangsnummer verkeerd ingevoerd PUK verkeerd ingevoerd. - onkostenbonnen - Onkostennota\'s tonen - onkostenbonnen + onkostenbewijzen + Bekijk kostenberekeningen + onkostenbewijzen Om onkostennota\'s te ontvangen, moet u verbonden zijn met de server. Aansluiten - Geen onkostennota\'s + Geen kostenbewijzen Deactiveren - Onderbreken - functie uitschakelen - Hiermee worden alle betalingsbewijzen van dit apparaat en van de server verwijderd. - Onkostennota\'s ontvangen - Ook uw kostenbonnen worden op de receptenserver bewaard. + Annuleren + Functie deactiveren + Hiermee worden alle onkostenbewijzen van dit apparaat en de server verwijderd. + Ontvang kostenbewijzen + Ook uw kostenberekeningen worden op de receptenserver opgeslagen. Ontvangen Totaal: %s %s Kiezen Splitsen - Blussen - Blussen + Verwijderen + Verwijderen Indienen %s € totale prijs - Tip: Dien declaraties in via de verzekeringsapp - Dien eenvoudig kostenbonnen in via de app van uw verzekeringsmaatschappij. Selecteer in de volgende stap deze app en druk op Delen. + Tip: Verstuur kostenbewijzen via de verzekeringenapp + Dien eenvoudig uw kostenberekeningen in via de app van uw verzekeraar. Selecteer in de volgende stap deze app en druk op delen. Oefening Apotheek Datum @@ -809,38 +808,64 @@ Medicijn-ID Afgegeven voor KVNR: %s - Geboortedatum: %s + Geboren op: %s OK - Hoe dient u bonnetjes in? - Zet direct over naar de app van uw verzekeringsmaatschappij/hulpkantoor. Selecteer hiervoor de app op de volgende pagina. + Hoe dient u bewijsstukken in? + Maak direct over naar de app van uw verzekerings-/uitkeringskantoor. Selecteer hiervoor de app op de volgende pagina. of - Sla het bestand op en importeer het later in het verzekerings-/hulpportaal. + Sla het bestand op en importeer het later in het verzekerings-/uitkeringsportaal. Artikel: %s - Getal: %s + Aantal: %s BTW: %s %% Brutoprijs in EUR: %s - Extra toeslagen - toeslag voor spoedeisende hulp + Extra kosten + Kosten voor noodservice BTM-vergoeding - T recept vergoeding - inkoop kosten + T-receptkosten + Inkoopkosten Koeriersdienst Totaal in EUR: %s heffing Echt verwijderen? - Het bestand wordt van uw apparaat en van de server verwijderd. - Blussen + Het bestand wordt van uw apparaat en de server verwijderd. + Verwijderen Geplaatst Postcode Plaats - Vul uw postcode in om contact met ons op te nemen. - Vul uw woonplaats in als u contact met ons opneemt. - Wordt voor je verzilverd - Is voor u verzilverd - U moet ingelogd zijn om van deze dienst gebruik te kunnen maken. - verzekering app - gezondheidskaart + Geef uw postcode op om contact met ons op te nemen. + Geef uw woonplaats op om contact met ons op te nemen. + Wordt voor u ingewisseld + Is voor u ingewisseld + Om van deze dienst gebruik te kunnen maken, moet u ingelogd zijn. + Verzekerings-app + Gezondheidskaart Bijbehorende pincode vereist + Alleen morgen te verzilveren als zelfbetaler + Nog maar %s dagen om in te wisselen als zelfbetaler + \nNog steeds inwisselbaar als zelfbetaler voor %s dagen\n + Alleen geldig voor %s dagen + \nGeldig voor nog %s dagen\n + Alleen morgen geldig + Kosten worden in rekening gebracht + Neemt een verzekering + De recept(en) zijn succesvol overgedragen. + Het recept kan niet worden verwerkt. Probeer het opnieuw. Mogelijk moet u een andere apotheek kiezen. + Het recept kan niet worden verwerkt. De apotheek meldt een onbekende fout. Probeer indien nodig een andere apotheek. + Het recept werd door de apotheek afgewezen. Het recept is mogelijk ongeldig of uw afleveradres of contactgegevens zijn mogelijk ongeldig. + Kan niet inwisselen. Controleer uw internetverbinding. + Het recept is succesvol overgedragen. De apotheek meldt echter een verwerkingsfout. Neem dan contact op met de apotheek. + Het recept werd door de apotheek afgewezen. Het recept is al ingewisseld. + Het recept werd door de apotheek afgewezen. Het recept is verwijderd. + Het recept kon niet worden overgedragen. Controleer uw internetverbinding en probeer het opnieuw. + Een of meer recepten konden niet worden overgedragen. + Fout bij verzenden + Succesvol verzonden! + Fout bij de apotheek + Fout bij de apotheek + Neem contact op met apotheek + Recept al ingewisseld + Recept verwijderd + Geen internet Om toegangslogboeken te ontvangen, moet u verbonden zijn met de server. Binnen deze termijn kunt u het recept nog steeds bij een apotheek invullen, maar u moet dan wel de volledige aankoopprijs van het medicijn zelf betalen. U kunt ook uw praktijk vragen om het recept opnieuw te laten verstrekken. Klaar @@ -849,4 +874,13 @@ In de app Laat deze code scannen bij uw apotheek. Factureringscorrectieverzoek + medicijn + Voer minimaal 1 teken in. + Of. Probeer de app in de demomodus + Demonstratie modus + Demonstratie modus + Gebruik de demomodus + Demomodus geactiveerd + Eindig hier + Activeer de demomodus diff --git a/android/src/main/res/values-pl/strings.xml b/app/features/src/main/res/values-pl/strings.xml similarity index 69% rename from android/src/main/res/values-pl/strings.xml rename to app/features/src/main/res/values-pl/strings.xml index d503c060..57d6f3a8 100644 --- a/android/src/main/res/values-pl/strings.xml +++ b/app/features/src/main/res/values-pl/strings.xml @@ -16,26 +16,26 @@ Kod recepty jest nieprawidłowy Ten kod recepty został już zeskanowany - %s recepta rozpoznana - %s recepty rozpoznane - %s recept rozpoznanych - %s recept rozpoznanych + %s przepis rozpoznany + + + Wykryto %s przepisów Anuluj Światło kamery - Czy anulować skanowanie kodów recept? - Anuluj skanowanie - Kontynuuj - Zaczynamy + Anulować skanowanie? + OK + Nie anuluj + Chodźmy Co jest potrzebne: Wprowadź numer dostępu do karty Wprowadź PIN Spróbuj ponownie Nie udało się utworzyć połączenia z serwerem. - Masz jeszcze %s próbę, zanim Twoja karta zostanie zablokowana. - Masz jeszcze %s próby, zanim Twoja karta zostanie zablokowana. - Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. + Masz jeszcze %s jeszcze jedną próbę, zanim Twoja karta zostanie zablokowana. + + Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. Numer dostępu znajduje się na górze z prawej strony Twojej karty zdrowia. @@ -71,7 +71,7 @@ Witamy Rozpocznij logowanie Odblokuj - Zaloguj się + Rejestr Anuluj Bezpieczeństwo Nota prawna @@ -81,25 +81,25 @@ Szczegóły Zaznacz jako zrealizowaną Zaznacz jako niezrealizowaną - Forma przekazania - standardowy rozmiar + Forma dawkowania + Rozmiar opakowania Osoba ubezpieczona - Nazwisko + Imię i nazwisko Adres Data urodzenia Ubezpieczenie zdrowotne / podmiot odpowiedzialny Status Numer ubezpieczonego Osoba wystawiająca receptę - Nazwisko + Imię i nazwisko Lekarz specjalista Numer lekarza (LANR) Instytucja - Nazwa + Imię i nazwisko Adres Numer zakładu pracy Numer telefonu - E-mail + Adres e-mail Wypadek przy pracy Data wypadku Numer przedsiębiorstwa, w którym nastąpił wypadek lub numer pracodawcy @@ -109,14 +109,14 @@ Godziny otwarcia Strona internetowa Możliwość zrealizowania jeszcze do dzisiaj jako płatnik indywidualny - Zaloguj się + Rejestr Aktywuj NFC Aktywuj funkcję NFC w swoim urządzeniu, aby zalogować się za pomocą swojej karty zdrowia. Aktywuj Skoryguj Czy zrealizowano recepty? Czy chcesz zaznaczyć recepty jako zrealizowane? - Niezrealizowane + Niezrealizowana Zrealizowane Otwarcie o godz. %s +49 800 277 377 7 @@ -125,10 +125,10 @@ Ustawienia Blokuj tworzenie zrzutów ekranu Zapobiega wyświetlaniu podglądu przy zmianie aplikacji - Czy zezwalasz aplikacji E-recepta na anonimową analizę Twojej aktywności? + Czy pozwalasz, aby E-Recepta analizowała Twoje zachowania w sposób anonimowy? Informacje techniczne Bezpieczeństwo danych Twojej recepty - Pamiętaj, że osoby, które oprócz Ciebie korzystają z Tego urządzenia i których cechy biometryczne mogą być zapisane w tym urządzeniu lub które znają PIN do urządzenia, kod logowania lub hasło, także uzyskają dostęp do Twoich recept. + Pamiętaj, że osoby, które oprócz Ciebie korzystają z Tego urządzenia i których cechy biometryczne mogą być zapisane w tym urządzeniu, także uzyskają dostęp do Twoich recept. Wysyłanie nie powiodło się Nie skonfigurowano programu poczty elektronicznej Brak wyników @@ -141,15 +141,15 @@ Chcę pomóc w ulepszaniu tej aplikacji Obejmuje to informacje o sprzęcie i oprogramowaniu w Twoim telefonie, ustawienia aplikacji E-recepta oraz zakres korzystania. Nigdy nie gromadzimy danych dotyczących Twojej osoby ani Twojego zdrowia. Dane są udostępniane przez podmiot przetwarzający wyłącznie firmie gematik GmbH i są usuwane najpóźniej po 180 dniach. Użytkownik może w każdej chwili dezaktywować analizę w menu aplikacji. - Na podstawie tych danych możemy sprawdzić, jakie funkcje są często używane, aby je usprawnić. Możemy także ocenić, jak długo musi być obsługiwana starsza technologia oraz kiedy np. możemy określić nowszą wersję systemu operacyjnego jako wymaganą, bez komplikacji dla (zbyt wielu) użytkowników. + Dane te pozwalają nam zrozumieć, które funkcje są często używane i ulepszać je. Możemy również oszacować, jak długo starsza technologia będzie wymagała wsparcia i kiedy możemy na przykład wprowadzić obowiązkową nowszą wersję systemu operacyjnego, nie wpływając na (zbyt wielu) użytkowników. Optymalizacja aplikacji Anonimowa analiza pozostaje nieaktywna %s Dziękujemy za Twoje wsparcie! - Zaloguj się + Rejestr Przeprowadź identyfikację, aby pobrać recepty. Wskazówka dla aptek:dane kontaktowe i informacje o aptekach pozyskujemy ze strony mein-apotkekenportal.de związku Deutscher Apothekenverband e.V. Znalazłeś(-aś) błąd lub chcesz skorygować dane? Dowiedz się więcej - Apteki + apteki Niestety nie udało się \uD83D\uDE15 Spróbuj ponownie. Wprowadź hasło @@ -177,13 +177,13 @@ Rozumiem Ponownie wprowadzone hasło zgadza się Błąd 20 10 76631 - Certyfikat Twojej karty zdrowia jest nieważny. Być może termin ważności Twojej karty już upłynął. Skontaktuj się ze swoją kasą chorych. - Bezskuteczne próby zalogowania się + Zaświadczenie o Twojej karcie zdrowia jest nieważne. Może Twoja karta utraciła ważność? Skontaktuj się ze swoją firmą zajmującą się ubezpieczeniem zdrowotnym. + Nieudane próby logowania - Stwierdzono %s bezskuteczną próbę zalogowania się. - Stwierdzono %s bezskuteczne próby zalogowania się. - Stwierdzono %s bezskutecznych prób zalogowania się. - + Wykryto %s nieudanych prób logowania. + + + Wykryto %s nieudanych prób logowania. Wybierz najlepsze zabezpieczenie urządzenia Takim zabezpieczeniem może być odcisk palca, wzór odblokowania itp. @@ -194,7 +194,7 @@ brak dostępnych tokenów SSP skopiowano do schowka Kliknij, aby skopiować token do schowka - Ważność jeszcze tylko do dzisiaj + Obowiązuje tylko dzisiaj Zezwól Brak połączenia z serwerem Spróbuj ponownie za kilka minut @@ -210,7 +210,7 @@ Błąd komunikacji z serwerem: Sprawdź połączenie internetowe i ustawienia daty/godziny. Ostrzeżenie Twoje urządzenie mogło mieć obniżone zabezpieczenia - Może to być spowodowane na przykład manipulowaniem urządzeniami lub aktywowanym trybem programisty. Ze względów bezpieczeństwa nie zalecamy korzystania z aplikacji na urządzeniach z jailbreakiem. + Może to być spowodowane na przykład manipulowaniem urządzeniami lub włączonym trybem programisty. Ze względów bezpieczeństwa nie zalecamy używania aplikacji na urządzeniach po jailbreaku. Akceptuję zwiększone ryzyko i mimo to chcę kontynuować. Dlaczego urządzenia z dostępem root stwarzają potencjalne zagrożenie dla bezpieczeństwa? Dowiedz się więcej @@ -239,7 +239,7 @@ Połączono Ostatnie połączenie dnia %s Usunąć profil? - Niniejsze dane profilu na tym urządzeniu zostaną usunięte. Twoje recepty w sieci medycznej zostaną zachowane. + Spowoduje to usunięcie wszystkich danych z profilu na tym urządzeniu. Twoje recepty w sieci opieki zdrowotnej zostaną zachowane. Usuń Anuluj Usuń profil @@ -257,25 +257,25 @@ Co się stanie, kiedy będę korzystać z funkcji kamery / odczytywać recepty za pomocą kamery? Brak nowych recept - %s nowa recepta - %s nowe recepty - %s nowe recepty - + %s nowy przepis + + + %s nowych przepisów - Gotowa do odbioru + Gotowe do odbioru W odkupieniu - Zrealizowana - Nieznana + Zrealizowane + Nieznane Wyświetl protokoły dostępu - Tutaj możesz zobaczyć, kto miał dostęp do Twoich recept - To jest kod dostępu do aplikacji E-recepta + Kto miał dostęp do Twoich recept + Kod dostępu do aplikacji E-recepta Protokoły dostępu Brak protokołów dostępu Brak jeszcze protokołów dostępu. Recepta jest teraz edytowana i nie można jej usunąć - Zaakceptuj + Zaakceptować Wygląda na to, że operacja nie powiodła się - Mamy świadomść, że nawiązywanie połączenia z kartą zdrowia ma swoje ukryte wady. Dlatego w przyszłości będzie można zarejestrować się również za pomocą już uwierzytelnionej aplikacji kasy chorych.\n\nPonadto pracujemy nad tym, aby można było realizować recepty online również bez rejestracji.\n\nCzy zauważyłeś(aś) podczas tego procesu coś, czym chciał(a)byś się z nami podzielić? Czekamy na Twoje opinie, również na krytyczne informacje zwrotne. + Jesteśmy świadomi, że połączenie z kartą zdrowia ma swoje pułapki. W przyszłości rejestracja powinna być możliwa również za pośrednictwem już uwierzytelnionej aplikacji ubezpieczenia zdrowotnego. \n\n Pracujemy również nad umożliwieniem realizacji recept cyfrowo bez konieczności rejestracji. \n\n Czy podczas tego procesu zauważyłeś coś, czym chciałbyś się z nami podzielić? Napisz do nas, chętnie otrzymamy również bardzo krytyczne uwagi. Porady dotyczące połączenia Zwiększ siłę połączenia Rozwiązaniem może być usunięcie osłonki. @@ -304,10 +304,10 @@ Zapisz na potrzeby kolejnych zamówień Zapisz recepty na urządzeniu - Kontynuuj z %s receptą - Kontynuuj z %s receptami + Kontynuuj według przepisu %s + - + Kontynuuj z %s przepisami Połączenie karty zdrowia nie powiodło się Aktualny profil jest już powiązany z inną kartą zdrowia (numer ubezpieczenia zdrowotnego %s). @@ -341,18 +341,18 @@ Nie możesz uzyskać dostępu? Sprawdź swoje biometryczne dane dostępowe na tym urządzeniu. Nie pamiętasz hasła? Usuń aplikację i następnie zainstaluj ją ponownie. Z naszego %s dowiesz się, dlaczego tak się dzieje. Pomoc - wielkość opakowania i jednostka + Wielkość opakowania i jednostka Substancja czynna Ilość substancji czynnej Oznaczenie serii Ważność do dnia Kategoria Szczepionka - Zaakceptuj + Zaakceptować Cofnij Wskazówka Pomóż nam ulepszyć tę aplikację - Wybierz własne hasło + Wprowadź hasło Hasło musi zawierać co najmniej osiem znaków Niewystarczająca siła hasła Wystarczająca siła hasła @@ -371,7 +371,7 @@ Aby dodać do swojej listy recepty, kliknij przycisk \"Skanuj\" w prawym górnym rogu. Zeskanuj wydruk papierowy Aby automatycznie otrzymywać recepty, musisz się zalogować. - Zaloguj się + Rejestr Brak zrealizowanych recept Tutaj zostaną wyświetlone Twoje zrealizowane recepty. Ze względu na ochronę danych Twoje recepty zostaną usunięte z serwera z receptami po 100 dniach. Brak zrealizowanych recept @@ -380,11 +380,11 @@ Podłączone urządzenia Zarejestrowane od %s (to urządzenie) Zarejestrowane od %s - Ze względów bezpieczeństwa połączenie z serwerem recept zostaje przerwane po 12 godzinach. Aby ponownie się połączyć, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. + Ze względów bezpieczeństwa połączenie z serwerem receptur zostaje zakończone po 12 godzinach. Aby ponownie nawiązać połączenie, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. PIN Wprowadź PIN (karty zdrowia) Dalej - Zaloguj sie + Rejestr Podłączone urządzenia Usunąć urządzenie? Anuluj @@ -407,28 +407,28 @@ Zebraliśmy kilka wskazówek, jak rozwiązać najczęściej występujące problemy. Wyświetl porady dotyczące połączenia Odblokuj - Karta została zablokowana + karta zablokowana Kod PIN został wprowadzony niepoprawnie trzy razy. Dlatego Twoja karta została zablokowana ze względów bezpieczeństwa. - Odblokuj kartę - Wprowadź PUK - Razem z kodem PIN otrzymałeś(aś) od swojej instytucji ubezpieczenia zdrowotnego 8-cyfrowy kod PUK. + odblokować kartę + Wpisz PUK + Wraz z kodem PIN otrzymałeś od swojej firmy ubezpieczeniowej 8-cyfrowy kod PUK. Wybierz nowy kod PIN - Możesz sam(a) wybrać swój nowy osobisty numer identyfikacyjny (PIN) (od 6 do 8 znaków). - Pamiętasz kod PIN? + Możesz samodzielnie wybrać swój nowy osobisty numer identyfikacyjny (PIN) (6 do 8 cyfr). + Zapamiętałeś kod PIN? Zanotuj swój kod PIN i przechowuj go w bezpiecznym miejscu. Anuluj OK Odblokowanie jest niemożliwe Za pomocą tego kodu PUK została wykorzystana maksymalna liczba odblokowań karty lub kod był wielokrotnie błędnie wprowadzany. Skontaktuj się ze swoim ubezpieczycielem. - Za pomocą kodu PUK możesz wykonać do 10 odblokowań. - Karta odblokowana + Możesz użyć jednego PUK do 10 odblokowań. + karta odblokowana Co jest potrzebne: Twoja karta zdrowia PUK do Twojej karty zdrowia Dalej - Karta zdrowia + Karta ubezpieczeniowa Zamów PIN lub kartę - Zaloguj się + Rejestr Jak chcesz się zalogować? Karta zdrowia obsługująca funkcję NFC PIN do karty zdrowia @@ -439,10 +439,10 @@ "Numer dostępu znajdziesz w prawym górnym rogu swojej karty zdrowia." Moja karta nie ma numeru dostępu - Masz jeszcze %s próbę, zanim Twoja karta zostanie zablokowana. - Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. + Masz jeszcze %s jeszcze jedną próbę, zanim Twoja karta zostanie zablokowana. + - + Masz jeszcze %s prób, zanim Twoja karta zostanie zablokowana. Przyłóż kartę zdrowia z tyłu telefonu Proces ten może potrwać do 30 sekund. @@ -464,7 +464,7 @@ Ważność upłynęła Zaloguj się za pomocą aplikacji Wybierz ubezpieczenie - Nie znalazłeś tego, czego szukałeś? Lista ta jest stale uzupełniana. Logowanie za pomocą karty zdrowia jest już obsługiwane przez każdą kasę chorych. + Nie znalazłeś tego, czego szukałeś? Lista ta jest stale poszerzana. Rejestrację z kartą zdrowia obsługuje już każda kasa chorych. Informacje zwrotne z aplikacji E-recepta Czekamy na Twój feedback. Postaraj się sformułować swoje opinie możliwie precyzyjnie i zapisz je poniżej: PUK @@ -476,29 +476,29 @@ Zapisz Nie zapisuj Wskazówka - Ze względów bezpieczeństwa połączenie z serwerem recept zostaje przerwane po 12 godzinach. Aby ponownie się połączyć, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. + Ze względów bezpieczeństwa połączenie z serwerem receptur zostaje zakończone po 12 godzinach. Aby ponownie nawiązać połączenie, potrzebujesz karty zdrowia i kodu PIN dla każdego procesu łączenia. Ustaw zabezpieczenie biometryczne Nie można zapisać danych dostępowych. Wcześniej utwórz zabezpieczenie biometryczne (np. odcisk palca) na swoim urządzeniu. Anuluj Ustawienia Wskazówka - Zaakceptuj + Zaakceptować Bezpieczeństwo danych Twojej recepty \"Ta aplikacja używa najbezpieczniejszego czujnika biometrycznego, jaki udostępnia Twoje urządzenie, aby zabezpieczyć Twoje dane dostępowe w chronionym obszarze pamięci urządzenia.\" Biometryczne zabezpieczenie Twoich danych dostępowych umożliwia otwieranie tej aplikacji w przyszłości bez karty zdrowia i podawania kodu PIN, a także przeglądanie, wywoływanie, realizowanie lub kasowanie recept. - Pamiętaj, że osoby, które oprócz Ciebie korzystają z Tego urządzenia i których cechy biometryczne mogą być zapisane w tym urządzeniu lub które znają PIN do urządzenia, kod logowania lub hasło, także uzyskają dostęp do Twoich recept. + Pamiętaj, że osoby, które oprócz Ciebie korzystają z Tego urządzenia i których cechy biometryczne mogą być zapisane w tym urządzeniu, także uzyskają dostęp do Twoich recept. Niestety nie udało się - Uwierzytelnienie za pomocą aplikacji kasy nie powiodło się. - Wygasł %s + Uwierzytelnienie w aplikacji ubezpieczenia zdrowotnego nie powiodło się. + Wygasło %s Przepis został już usunięty z serwera - Popraw wprowadzone dane lub odrzuć zmiany + Popraw swój wpis lub odrzuć zmiany Skoryguj Dane ubezpieczonego Imię i nazwisko Ubezpieczenie Numer ubezpieczonego Numer dostępu do karty - Zaloguj się + Rejestr Wyloguj Zapisz Zmiana @@ -507,87 +507,87 @@ Serwer nie odpowiada Spróbuj ponownie później. Spróbuj ponownie - Poszukaj ubezpieczenia - Połączyć się teraz z serwerem receptur? + Wyszukaj ubezpieczenie + Połączyć się teraz z serwerem przepisów? zalogowano pomyślnie utracono połączenie - Połączyć się teraz z serwerem receptur? + Połączyć się teraz z serwerem przepisów? Brak tokenów - Token otrzymasz po zalogowaniu się do obsługi recept.\n + Token otrzymasz po zalogowaniu się do serwisu recept.\n Zamówienia Wybierz żądany kod PIN - Odblokuj kartę + odblokować kartę Wybierz PIN Powtórz PIN - Wprowadzone hasła różnia się od siebie. + Wpisy różnią się od siebie. Brak zamówień Nie masz jeszcze żadnych zamówień. Właśnie O godzinie %s Koszyk jest gotowy - Przepis został dodany do Twojego koszyka. Wejdź na stronę apteki, aby sfinalizować zamówienie. + Przepis został dodany do Twojego koszyka. Aby dokończyć zamówienie, należy wejść na stronę apteki. Otwórz koszyk - Pokaż ten kod odbioru w aptece. + Pokaż ten kod kolekcji w aptece. Otrzymano kod odbioru Wiadomość nie może zostać wyświetlona - Skontaktuj się ze swoją apteką ( %s ). + Proszę skontaktować się ze swoją apteką ( %s ). Pokaż link do koszyka Wyświetl kod odbioru Pokaż wiadomość %s o godzinie %s - Przepis wysłano do %s . - Przegląd zamówień + Przepis wysłany do %s . + Przegląd zamówienia Nowy Kurs - porządek - Bezpłatnie dla dzwoniącego. Godziny obsługi: pon. - pt. 8:00 - 20:00 z wyjątkiem świąt państwowych + Kolejność + Bezpłatnie dla dzwoniącego. Godziny świadczenia usług: Pon. - Pt. 8:00 - 20:00 z wyjątkiem świąt państwowych Apteka Wybierz żądany kod PIN Zapisano żądany kod PIN Obecnie otwarte i blisko mnie Filtruj według … Zacznij szukać - bezpośrednie przypisanie + Przypisanie bezpośrednie apteki - numer telefonu (opcjonalnie) + Numer telefonu (opcjonalnie) Szukaj według nazwy lub adresu - Brak prawidłowych informacji o aptece + Brak aktualnych informacji o aptece Nie znaleziono aktualnych informacji o tej aptece. Wpis dotyczący tej apteki zostanie usunięty. OK - Katalog aptek jest niedostępny - Obecnie nie można uzyskać aktualnych informacji o tej aptece. Proszę sprawdzić swoje połączenie z internetem. + Katalog aptek nie jest dostępny + Obecnie nie są dostępne żadne aktualne informacje na temat tej apteki. Proszę sprawdzić swoje połączenie z internetem. Anuluj Spróbuj ponownie - Zapisz środowisko - Nie można się zalogować - Wygląda na to, że Twoje dane logowania biometrycznego uległy zmianie. Zarejestruj się ponownie za pomocą swojej karty zdrowia. + Oszczędzaj środowisko + Logowanie nie jest możliwe + Wygląda na to, że Twoje dane logowania biometrycznego uległy zmianie. Proszę zalogować się ponownie przy użyciu swojej karty zdrowia. Anuluj - Zaloguj się - profil 1 + Rejestr + Profil 1 Blisko mnie - Do wykorzystania później + Możliwość wykorzystania później Do wykorzystania od %s - ulepszenia produktu + Ulepszenia produktu Anonimowa analiza - Pomóż nam ulepszyć tę aplikację. Wszystkie dane użytkownika są zbierane anonimowo i służą wyłącznie do poprawy komfortu użytkowania. - bezpieczeństwo urządzenia + Pomóż nam ulepszyć tę aplikację. Wszystkie dane dotyczące użytkowania są gromadzone anonimowo i wykorzystywane wyłącznie w celu poprawy komfortu użytkowania. + Bezpieczeństwo urządzenia ustawienia osobiste Pomoc w obsłudze - ulepszenia produktu + Ulepszenia produktu Dodano przepis Przepis już dostępny Wystąpił błąd podczas importowania Usuń Zeskanowana recepta Możliwość wyboru preparatu zastępczego - Nie pamiętam kodu PIN + Zapomniany PIN - %s recepta + %s Przepis - %s recepty + %s Przepisy - Zapoznałem się i akceptuję politykę prywatności i warunki użytkowania. + Przeczytałem i akceptuję politykę prywatności i warunki użytkowania. Polityka prywatności Warunki korzystania Chcielibyśmy: @@ -596,117 +596,116 @@ Wszystkie dane są oczywiście zbierane anonimowo. Możesz w każdej chwili zmienić tę decyzję w ustawieniach systemowych. Kontynuuj - Zaakceptuj - Ta aplikacja korzysta z najbezpieczniejszej metody dostępnej na Twoim urządzeniu. + Zaakceptować + Ta aplikacja wykorzystuje najbezpieczniejszą metodę udostępnianą przez Twoje urządzenie. Zapisz Wybierać Lek Nazwa handlowa - TAk - nie + Tak + NIE dawkowanie Data wydania Ta recepta zostanie zrealizowana dla Ciebie w ramach leczenia. Brak danych - dodatkowa opłata + Dodatkowa opłata Lek - Dokumenty dostawy - Kwalifikuje się zgodnie z BVG - przygotowanie alternatywne - nazwa formuły - Opakowania - instrukcja rzemieślnicza - opis + Instrukcje dotyczące składania + Kwalifikuje się według BVG + Przygotowanie alternatywne + Nazwa przepisu + Opakowanie + Instrukcje produkcyjne + Opis podane przez - wydany w: + wydane w: Substancja czynna - przepisany + Przepisane Odbierać - Czym jest przydział bezpośredni? - W przypadku skierowań bezpośrednich, receptę z przychodni lub szpitala realizuje się bezpośrednio w aptece. Ubezpieczeni nie muszą podejmować żadnych działań i nie mogą ingerować w proces wykupu. \n\n Bezpośrednie skierowania są wymienione w aplikacji e-recept, aby Twoje leczenie było dla Ciebie bardziej przejrzyste. + Co to jest zlecenie bezpośrednie? + W przypadku bezpośredniego skierowania recepta z przychodni lub szpitala realizowana jest bezpośrednio w aptece. Osoby ubezpieczone nie muszą podejmować żadnych działań i nie mogą ingerować w proces umorzenia. \n\n Bezpośrednie skierowania są wymienione w aplikacji do e-recepty, dzięki czemu leczenie jest dla Ciebie bardziej przejrzyste. Opłata za obsługę w nagłych wypadkach Czasami zachodzi potrzeba pośpiechu. Niektóre recepty można zrealizować bez dodatkowej opłaty za pogotowie, na przykład w nocy lub w dni wolne od pracy. Leki objęte współpłatnością - Zwolnione ze współpłacenia - Osoby posiadające ustawowe ubezpieczenie zdrowotne muszą zapłacić do dziesięciu euro za leki na receptę. \n\n Wysokość dopłaty zależy od ceny leku. Sam musisz zapłacić za leki, które kosztują mniej niż 5 euro.\n W przypadku droższych leków trzeba zapłacić dziesięć procent ceny, ale co najmniej 5 euro, a maksymalnie 10 euro. \n\n Dzieci i młodzież poniżej 18 roku życia są z reguły zwolnione ze współpłacenia. \n\n Jeśli Twoje roczne koszty leków przekraczają Twój limit finansowy, możesz zostać zwolniony ze współpłacenia. Porozmawiaj o tym ze swoim ubezpieczycielem zdrowotnym. - Jesteś zwolniony ze współpłacenia tego leku. Twoje ubezpieczenie zdrowotne pokryje koszty leków. - Jak długo ta recepta jest ważna? + Zwolnione z dodatkowej opłaty + Osoby posiadające ustawowe ubezpieczenie zdrowotne muszą uiścić dodatkową opłatę w wysokości do dziesięciu euro za leki na receptę. \n\n Wysokość dopłaty uzależniona jest od ceny leku. Za leki, które kosztują mniej niż 5 euro, musisz sam zapłacić.\n Za droższe leki trzeba zapłacić dziesięć procent ceny, ale co najmniej 5 euro, a maksymalnie 10 euro. \n\n Dzieci i młodzież do lat 18 są co do zasady zwolnione z dodatkowej opłaty. \n\n Jeśli Twoje roczne koszty leków przekraczają Twój limit obciążeń finansowych, możesz zostać zwolniony ze współpłacenia. Porozmawiaj o tym ze swoją firmą ubezpieczeniową. + Jesteś zwolniony z współpłacenia za ten lek. Koszt leku pokryje Twoja firma ubezpieczeniowa. + Jak długo ważna jest ta recepta? W tym okresie możesz zrealizować receptę w dowolnej aptece za maksymalną dopłatą w wysokości 10 €. Możliwość wyboru preparatu zastępczego - Ze względu na wymagania prawne Twojej firmy ubezpieczeniowej, możesz otrzymać alternatywę z tą samą substancją czynną. \n\n Leki mogą wyglądać i nazywać się inaczej, mieć różne ceny i producentów, a mimo to zawierają ten sam składnik aktywny. Sam składnik aktywny i dawkowanie są szczególnie ważne dla działania leków na organizm. Pacjenci w aptece często otrzymują inny lek niż ten przepisany przez lekarza na receptę – pod warunkiem, że leki są porównywalne. Zmiana może mieć przyczyny terapeutyczne i ekonomiczne. + Ze względu na wymogi prawne nałożone przez firmę ubezpieczeniową możesz otrzymać lek alternatywny zawierający tę samą substancję czynną. \n\n Leki mogą wyglądać i nazywać się inaczej, mieć różnych cen i producentów, a mimo to zawierać tę samą substancję czynną. Decydujące znaczenie dla działania leków na organizm ma sam składnik aktywny oraz jego dawkowanie. Pacjenci często otrzymują w aptece inny lek niż ten przepisany przez lekarza – pod warunkiem, że lek jest porównywalny. Zmiana może mieć podłoże terapeutyczne i ekonomiczne. Zeskanowana recepta - Ze względów bezpieczeństwa recepty importowane z wydruku papierowego nie mogą zawierać żadnych danych osobowych ani medycznych. \n\n Zaloguj się do tej aplikacji za pomocą karty zdrowia lub aplikacji ubezpieczeniowej, aby wyświetlić wszystkie informacje zawarte na recepcie. - Recepta błędna - Ta recepta została wystawiona nieprawidłowo. - Zeskanowana recepta - opłata za usługi ratunkowe + Ze względów bezpieczeństwa recepty importowane z wersji papierowej nie mogą zawierać danych osobowych ani medycznych. \n\n Zaloguj się do tej aplikacji za pomocą karty zdrowia lub aplikacji ubezpieczeniowej, aby wyświetlić wszystkie informacje zawarte na recepcie. + Przepis błędny + Recepta ta została wystawiona błędnie. + Opłata za obsługę w nagłych wypadkach Dawkowanie zgodnie z pisemną instrukcją Telefon - strona + strona internetowa E-mail Sortowanie według odległości nie jest możliwe. OK Wpisz aktualny kod PIN - Wprowadzono nieprawidłowy kod PIN + Wprowadzono błędny PIN Aktualny PIN Twojej karty zdrowia - Karta została zablokowana + karta zablokowana Odblokuj kartę w Ustawienia > Odblokuj kartę. Ze względów bezpieczeństwa wprowadź aktualny kod PIN. - Nie pamiętam kodu PIN + Zapomniany PIN Błędna recepta Lek Wygląda na to, że podczas wystawiania Twojej recepty coś poszło nie tak. Czy zgłosić błąd? Zgłoś Nie zalogowano Zarejestrowany z - Karta zdrowia + Karta ubezpieczeniowa Biometria Nie zalogowano - Interesuje nas Twoja opinia. Poświęć pięć minut na wypełnienie naszej ankiety. Z góry dziękuję. - ostrzeżenie + Jesteśmy ciekawi Twojej opinii. Prosimy o poświęcenie pięciu minut na wypełnienie naszej ankiety. Z góry bardzo dziękuję. + Ostrzeżenie Apteka dodana do ulubionych - Usunięto aptekę z ulubionych + Apteka usunięta z ulubionych Moje apteki Bardzo dobra siła hasła Operacja zapisu nie powiodła się - Nie udało się zapisać kodu PIN + Nie można zapisać kodu PIN Zgłoś Przypisz PIN - Naruszono regułę dostępu + Naruszona zasada dostępu Nie masz uprawnień dostępu do katalogu map. - Przypisz swój własny kod PIN - Karta jest zabezpieczona kodem PIN z kasy chorych (transport PIN), prosimy o nadanie własnego kodu PIN. + Przypisz własny kod PIN + Karta zabezpieczona jest PIN-em wydanym przez Twoje ubezpieczenie zdrowotne (PIN transportowy) Prosimy o wpisanie własnego PIN-u. Nie znaleziono hasła Na Twojej karcie nie jest zapisane żadne hasło. Zostałeś wylogowany - Zaloguj się ponownie, aby zaktualizować swoje recepty. - numer składnika aktywnego + Zaloguj się ponownie, aby zaktualizować swoje przepisy. + Numer składnika aktywnego moc i jedność Wykorzystano %s minut temu - Wykorzystano %s - Odkupiony przed chwilą - Wykorzystane o godzinie %s . + Wykorzystano w dniu %s + Odkupiony właśnie teraz + Wykorzystano o godzinie %s Zamówienia Ta recepta została zrealizowana dla Ciebie w ramach leczenia. - opłata za pogotowie - Recepty tej nie można zrealizować w aptece w nocy bez uiszczenia dodatkowej opłaty za pogotowie. + Opłata za obsługę w nagłych wypadkach + Recepty tej nie można zrealizować w aptece w godzinach nocnych bez dodatkowej opłaty za pogotowie. Szukaj tutaj Ustawienia - Udostępnij lokalizację w ustawieniach. + Udostępnij lokalizację w Ustawieniach. Blisko mnie - Przytrzymaj, aby edytować nazwę. + Naciśnij i przytrzymaj, aby edytować nazwę. Wprowadź nową nazwę profilu. - Musisz być zalogowany, aby otrzymywać cyfrowe recepty ze swojej praktyki. - Odbierać recepty cyfrowo? - Przeciągnij ekran w dół, aby odświeżyć. + Aby otrzymywać recepty w formie cyfrowej ze swojej przychodni, musisz się zalogować. + Otrzymywać recepty cyfrowo? + Pociągnij ekran w dół, aby odświeżyć. Brak recept - Dodaj recepty za pomocą przycisku + w prawym górnym rogu. - Zaloguj sie - archiwum recept + Dodaj przepisy za pomocą przycisku + w prawym górnym rogu. + Rejestr + Archiwum przepisów Może później - Zaloguj sie + Rejestr Edytuj zdjęcie profilowe - archiwum recept + Archiwum przepisów Podaj nazwę Zapisz Moje zamówienie @@ -714,100 +713,100 @@ Recepty Apteka Wysłać - Zmienić + Zmiana Odbierz w aptece Dostawa kurierem - Dostawa pocztą - %s recept - Odkupienie niemożliwe + Dostawa wysyłkowa + %s Przepisy + Nie ma możliwości wykupienia Nie można zrealizować jednej lub więcej recept. - Nie wybrano recepty - Aby zrealizować recepty, należy wybrać co najmniej jedną receptę. - Dodaj informacje kontaktowe - Zmienić - Bez recepty - Obecnie nie masz żadnych recept do zrealizowania - ulec poprawie - kurier + Nie wybrano przepisu + Aby skorzystać z przepisów, należy wybrać co najmniej jeden przepis. + Dodaj dane kontaktowe + Zmiana + Brak przepisu + Obecnie nie masz żadnych recept, które można zrealizować + kolekcja + dostawca Wysyłka - wybieraj recepty - Kliknij tutaj, aby zeskanować recepty + Wybierz przepisy + Kliknij tutaj, aby zeskanować przepisy Naciśnij długo, aby edytować nazwy Dodaj więcej profili, np. dla swoich dzieci lub rodziców - Kliknij wyświetlacz, aby pominąć wyświetloną wskazówkę narzędzia. - Jak odkupić? - W jaki sposób chciałbyś otrzymywać leki? + Kliknij ekran, aby pominąć wyświetlaną podpowiedź. + Jak wykupić? + W jaki sposób chcesz otrzymać leki? Zrealizuj bezpośrednio Zrealizuj leki na miejscu Zamów - Zarezerwuj lub zamów dostawę + Zarezerwuj lub zleć dostawę Gotowe - kodeks zbiorowy - pojedyncze kody + Kod kolekcji + Indywidualne kody - Masz receptę %s . + Masz %s przepis. - Masz %s recept. + Masz %s przepisów. Wybierz - Wszystkie recepty - Jakie recepty? + Wszystkie przepisy + Które przepisy? Dalej Dalej Dowiedz się więcej Wskazówka - Ta aplikacja używa oprogramowania Google do rozpoznawania kodów. + Ta aplikacja korzysta z oprogramowania firmy Google do rozpoznawania kodów. Dowiedz się więcej - O skanerze kodów recept - Jakie dane zawiera kod recepty? - Kod recepty zawiera tylko identyfikator recepty. Dzięki temu receptę można znaleźć w serwisie recept w cyfrowej sieci zdrowia. Kod recepty nie zawiera żadnych danych o Tobie ani o Twoim leku. - Więc nikt nie może nic zrobić z samym kodem recepty? - Prawidłowy. Dane recepty należy pobrać z serwisu recept. Wymaga to bezpiecznego logowania. + Informacje o skanerze kodów receptur + Jakie dane zawiera kod przepisu? + Kod przepisu zawiera jedynie identyfikator przepisu. Oznacza to, że receptę można znaleźć w serwisie recept w cyfrowej sieci opieki zdrowotnej. Kod recepty nie zawiera żadnych informacji o Tobie ani o Twoich lekach. + Czyli nikt nie może nic zrobić z samym kodem przepisu? + Prawidłowy. Dane dotyczące recept należy pobrać w serwisie recept. Wymagane jest do tego bezpieczne logowanie. Kto może zarejestrować się w usłudze recepty? - Rejestracja w serwisie recept w cyfrowej sieci zdrowia jest możliwa dla ubezpieczonych, aptek, praktyk lekarskich i szpitali. + Rejestracja do usługi receptowej w cyfrowej sieci zdrowia możliwa jest dla ubezpieczonych, aptek, przychodni i szpitali. Dlaczego aplikacja e-recepta korzysta z funkcji Google? - Google oferuje funkcje, które można łatwo zintegrować z aplikacjami i które są stale rozwijane i aktualizowane przez Google. Gwarantuje to, że funkcje działają na wielu różnych urządzeniach końcowych i mogą być bezpiecznie obsługiwane. Aplikacja korzysta z funkcji poprawiającej funkcjonalność aparatu i skanowania dla urządzeń z systemem Android (Google ML Kit). - Jak działa ulepszenie skanowania Google ML Kit? - Google ML Kit pomaga zoptymalizować obraz przechwytywany przez aparat, tak aby kody recept można było odczytać nawet w złych warunkach oświetleniowych lub przy użyciu starszych modeli aparatów. - Czy dane dotyczące recepty lub mojego leku zostaną przekazane do Google? - NIE. Odczytany kod recepty jest zapisywany bezpośrednio w aplikacji i nie jest przekazywany do Google. Dane recepty nie są przechowywane w kodzie, tylko w cyfrowej sieci zdrowia. Stamtąd są one przesyłane do aplikacji. Google nie ma dostępu do cyfrowej sieci zdrowia. - Jakie dane przetwarza Google podczas korzystania z zestawu ML Kit? - Google ma dostęp wyłącznie do informacji technicznych dotyczących używanego urządzenia końcowego i ogólnego korzystania z funkcji dodatkowej (np. współczynnika błędów, ustawień aparatu) w celu rejestrowania tego statystycznie i ulepszania w ten sposób funkcji dodatkowej. Podczas uzyskiwania dostępu Google tymczasowo zapisuje adres IP urządzenia końcowego. Informacje o Tobie i treść recepty nie będą rejestrowane przez Google. + Google oferuje funkcje, które można łatwo zintegrować z aplikacjami i które Google stale rozwija i aktualizuje. Dzięki temu funkcje działają na wielu różnych urządzeniach i można ich bezpiecznie używać. Aplikacja wykorzystuje funkcję poprawiającą funkcjonalność aparatu i skanowania na urządzeniach z systemem Android (Google ML Kit). + Jak usprawnienie skanowania działa z zestawem Google ML Kit? + Zestaw Google ML pomaga zoptymalizować obraz rejestrowany przez kamerę, dzięki czemu kody receptur można odczytać nawet przy słabym oświetleniu lub przy użyciu starszych modeli kamer. + Czy dane dotyczące recepty lub mojego leku zostaną udostępnione Google? + NIE. Odczytany kod przepisu jest zapisywany bezpośrednio w aplikacji. Nie zostanie udostępniony Google. Dane dotyczące recept nie są przechowywane w kodzie, a jedynie w cyfrowej sieci opieki zdrowotnej. Stamtąd są przesyłane do aplikacji. Google nie ma dostępu do cyfrowej sieci opieki zdrowotnej. + Jakie dane przetwarza Google podczas korzystania z ML Kit? + Google uzyskuje dostęp do informacji technicznych dotyczących używanego urządzenia i ogólnego korzystania z funkcji dodatkowej (np. współczynnika błędów, ustawień aparatu) wyłącznie w celu statystycznego zarejestrowania tego i ulepszenia funkcji dodatkowej. Podczas uzyskiwania dostępu Google tymczasowo rejestruje adres IP Twojego urządzenia. Informacje o Tobie i treść przepisu nie są rejestrowane przez Google. Czy korzystanie z Google ML Kit jest dobrowolne? - Tak Jednak ML Kit jest wbudowany w skaner kodów recept w wersji aplikacji e-recepty na Androida.Jeśli używasz skanera kodów recept na urządzeniu z Androidem, funkcja ML Kit jest również zawsze używana. Możesz jednak obejść się bez użycia skanera kodów recept. Twoje recepty można również załadować do aplikacji, jeśli zarejestrujesz się w cyfrowej sieci zdrowia za pomocą elektronicznej karty zdrowia lub za pośrednictwem aplikacji ubezpieczenia zdrowotnego. - Czy mogę zobaczyć, kto przeglądał moje recepty? - Tak. Cały dostęp do twoich danych jest w pełni rejestrowany w cyfrowej sieci zdrowia. W aplikacji e-recepta możesz zobaczyć, kto miał dostęp do Twoich danych. - Z kim mogę się skontaktować, jeśli mam pytania dotyczące aplikacji lub e-recepty? + Tak. Jednak ML Kit jest wbudowany w skaner kodów receptur w wersji aplikacji e-recepty na Androida. Jeśli korzystasz ze skanera kodów receptur na urządzeniu z systemem Android, zawsze używana jest funkcja ML Kit. Można jednak zrezygnować ze skanera kodów receptur. Twoje recepty można również załadować do aplikacji, jeśli zalogujesz się do cyfrowej sieci opieki zdrowotnej za pomocą elektronicznej karty zdrowia lub aplikacji ubezpieczenia zdrowotnego. + Czy mogę zobaczyć, kto przeglądał moje przepisy? + Tak. Cały dostęp do Twoich danych jest w pełni rejestrowany w cyfrowej sieci opieki zdrowotnej. W aplikacji e-recepty możesz sprawdzić, kto miał dostęp do Twoich danych. + Gdzie mogę się skontaktować, jeśli mam pytania dotyczące aplikacji lub e-recepty? Szczegółowe informacje można znaleźć w oświadczeniu o ochronie danych. - Przepisana liczba opakowań + Liczba przepisanych opakowań Brak recept - W tym celu potrzebujesz recept podlegających zwrotowi. + W tym celu potrzebne są wymienialne recepty. Wybierz ubezpieczenie - Szukaj ubezpieczenia + Wyszukaj ubezpieczenie Anuluj O co chcesz wnioskować? - Do tej aplikacji potrzebujesz karty i powiązanego kodu PIN. - Jak chcesz się skontaktować z firmą ubezpieczeniową? - Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu - Twoja firma ubezpieczeniowa oferuje następujące opcje kontaktu + Do tej aplikacji potrzebujesz karty i powiązanego z nią kodu PIN. + Jak chcesz skontaktować się ze swoją firmą ubezpieczeniową? + Twoja firma ubezpieczeniowa oferuje następujące możliwości kontaktu + Twoja firma ubezpieczeniowa oferuje następującą możliwość kontaktu Zamknij - Kod PIN wprowadzony nieprawidłowo. - Numer dostępu wpisany nieprawidłowo - Kod PUK został wprowadzony nieprawidłowo. - rachunki za wydatki - Pokaż rachunki za wydatki - rachunki za wydatki - Aby otrzymywać rachunki za wydatki, musisz mieć połączenie z serwerem. + PIN wpisany niepoprawnie. + Numer dostępowy wpisany niepoprawnie + PUK wpisany błędnie. + wpływy z wydatków + Zobacz rachunki kosztów + wpływy z wydatków + Aby otrzymywać rachunki za wydatki, musisz być podłączony do serwera. Nawiąż połączenie - Brak rachunków za wydatki + Żadnych rachunków Dezaktywuj Anuluj - wyłączyć funkcję - Spowoduje to usunięcie wszystkich paragonów z tego urządzenia iz serwera. - Otrzymuj rachunki za wydatki - Rachunki poniesionych kosztów są również zapisywane na serwerze recept. - Odbierać + Dezaktywuj funkcję + Spowoduje to usunięcie wszystkich rachunków za wydatki z tego urządzenia i serwera. + Otrzymuj rachunki za koszty + Twoje rachunki kosztów są również zapisywane na serwerze receptur. + Otrzymane Razem: %s %s Wybierz Podział @@ -816,8 +815,8 @@ Składać %s € cena całkowita - Wskazówka: prześlij rachunki za wydatki za pośrednictwem aplikacji ubezpieczeniowej - Łatwe przesyłanie rachunków kosztów za pośrednictwem aplikacji firmy ubezpieczeniowej. W następnym kroku wybierz tę aplikację i naciśnij Udostępnij. + Wskazówka: przesyłaj rachunki za koszty za pośrednictwem aplikacji ubezpieczeniowej + Z łatwością przesyłaj rachunki za koszty za pośrednictwem aplikacji firmy ubezpieczeniowej. W następnym kroku wybierz tę aplikację i naciśnij udostępnij. Ćwiczyć Apteka Data @@ -825,38 +824,64 @@ Identyfikator leku Wydana dla KVNR: %s - Data urodzenia: %s + Urodzony: %s OK - Jak przesyłać rachunki? - Przenieś bezpośrednio do aplikacji swojej firmy ubezpieczeniowej / biura pomocy. Aby to zrobić, wybierz aplikację na następnej stronie. + Jak przesłać dokumenty uzupełniające? + Przenieś bezpośrednio do aplikacji swojego biura ubezpieczeń/zasiłków. Aby to zrobić, wybierz aplikację na następnej stronie. Lub - Zapisz plik, a następnie zaimportuj go do portalu ubezpieczenia/pomocy. + Zapisz plik, a następnie zaimportuj go do portalu ubezpieczeń/świadczenia. Artykuł: %s - Numer: %s + Liczba: %s VAT: %s %% Cena brutto w EUR: %s Dodatkowe opłaty Opłata za obsługę w nagłych wypadkach Opłata BTM - Opłata za receptę T - koszty zaopatrzenia + T-opłata za receptę + Koszty zakupu Usługa kurierska - Suma w EUR: %s + Razem w EUR: %s nałożyć Naprawdę usunąć? Plik zostanie usunięty z Twojego urządzenia i serwera. Usuń - wysłane + Wysłano Kod pocztowy Miejscowość - Wprowadź swój kod pocztowy, aby się z nami skontaktować. - W przypadku kontaktu z nami prosimy o podanie miejsca zamieszkania. + Podaj swój kod pocztowy, aby się z nami skontaktować. + Aby się z nami skontaktować, prosimy o podanie miejsca zamieszkania. Zostanie dla ciebie odkupiony Został odkupiony dla ciebie - Aby korzystać z tej usługi, musisz być zalogowany. + Aby móc korzystać z tej usługi, musisz być zalogowany. Aplikacja ubezpieczeniowa Karta ubezpieczeniowa Wymagany powiązany kod PIN + Można wykorzystać dopiero jutro jako osoba płacąca samodzielnie + Pozostało tylko %s dni do wykorzystania jako osoba płacąca samodzielnie + \nNadal można go wykorzystać jako osoba samopłatna przez %s dni\n + Ważne tylko przez %s dni + \nObowiązuje przez %s dni\n + Obowiązuje tylko jutro + Za opłatą + Bierze ubezpieczenie + Przepisy zostały pomyślnie przesłane. + Nie można przetworzyć przepisu. Proszę spróbuj ponownie. Być może będziesz musiał wybrać inną aptekę. + Nie można przetworzyć przepisu. Apteka zgłasza nieznany błąd. Jeśli to konieczne, spróbuj innej apteki. + Recepta została odrzucona przez aptekę. Recepta może być nieważna lub Twój adres dostawy lub dane kontaktowe mogą być nieprawidłowe. + Nie można zrealizować kuponu. Sprawdź swoje połączenie internetowe. + Przepis został pomyślnie przesłany. Apteka zgłasza jednak błąd w przetwarzaniu. Prosimy o kontakt z apteką. + Recepta została odrzucona przez aptekę. Recepta została już zrealizowana. + Recepta została odrzucona przez aptekę. Przepis został usunięty. + Nie udało się przenieść przepisu. Sprawdź swoje połączenie internetowe i spróbuj ponownie. + Nie można przenieść jednego lub więcej przepisów. + Błąd podczas wysyłania + Wysłano pomyślnie! + Błąd w aptece + Błąd w aptece + Skontaktuj się z apteką + Recepta już zrealizowana + Przepis usunięty + Brak internetu Aby otrzymać logi dostępowe musisz być połączony z serwerem. W tym okresie nadal możesz zrealizować receptę w aptece, ale całą cenę zakupu leku będziesz musiał sam zapłacić. Alternatywnie możesz poprosić swoją praktykę o ponowne wystawienie recepty. Gotowe @@ -865,4 +890,13 @@ W aplikacji Zeskanuj ten kod w swojej aptece. Żądanie korekty płatności + Lek + Proszę wpisać przynajmniej 1 znak. + Lub. Wypróbuj aplikację w trybie demonstracyjnym + Tryb demonstracyjny + Tryb demonstracyjny + Skorzystaj z trybu demonstracyjnego + Aktywowano tryb demonstracyjny + Zakończ tutaj + Aktywuj tryb demonstracyjny diff --git a/android/src/main/res/values-pl/strings_kbv_codes.xml b/app/features/src/main/res/values-pl/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values-pl/strings_kbv_codes.xml rename to app/features/src/main/res/values-pl/strings_kbv_codes.xml diff --git a/android/src/main/res/values-ro/strings.xml b/app/features/src/main/res/values-ro/strings.xml similarity index 66% rename from android/src/main/res/values-ro/strings.xml rename to app/features/src/main/res/values-ro/strings.xml index 382d8297..b9388b69 100644 --- a/android/src/main/res/values-ro/strings.xml +++ b/app/features/src/main/res/values-ro/strings.xml @@ -1,17 +1,17 @@ Bine - Întrerupe - Întoarcere + Anulare + Înapoi în jurul Digital. Rapid. Sigur. ID-ul sarcinii - cod de acces + Cod de acces Termeni de utilizare Protejarea datelor Rețete - Accesul la cameră a fost interzis - Pentru a utiliza scanerul, trebuie să permiteți accesul aplicației la camera dvs. în setările de sistem. + Accesul la cameră interzis + Pentru a utiliza scanerul, trebuie să permiteți aplicației să vă acceseze camera în Setările de sistem. Focalizează camera pe un cod de rețetă Acesta nu este un cod de prescripție valid Acest cod de rețetă a fost deja scanat @@ -20,57 +20,57 @@ %s rețete recunoscute - Întrerupe - lumina camerei + Anulare + Lumina camerei Anulați scanarea? Bine Nu anulați - Începem + Să mergem De ce ai nevoie: Introduceți numărul de acces al cardului introduceți codul PIN - încearcă din nou + Încearcă din nou Eroare de conectare la server. - Mai aveți %s încercări înainte ca cardul dvs. să fie blocat. + Mai aveți %s o încercare înainte ca cardul să fie blocat. Mai aveți %s încercări înainte ca cardul dvs. să fie blocat. - Numărul de acces îl veți găsi în partea dreaptă sus a cardului dumneavoastră de sănătate. - Întrerupe - Caută harta... - Țineți cardul de sănătate în spatele dispozitivului. + Numărul de acces îl găsiți în partea dreaptă sus a cardului dumneavoastră de sănătate. + Anulare + Căutați după hartă... + Țineți cardul de sănătate pe spatele dispozitivului. Încă mai caut… - Mutați încet cardul din spatele dispozitivului. + Mutați încet cardul pe spatele dispozitivului. Bacsis Carcasele dispozitivului pot îngreuna conectarea prin NFC. - card recunoscut + Card recunoscut Încercați să nu mutați cardul de sănătate. Carte de sănătate găsită. Te rog nu te mișca. - conexiunea pierdută + conexiune pierdută Țineți cardul de sănătate pe spatele dispozitivului din nou Versiune: %s - Build Hash: %s - meniul de depanare + Creați hash: %s + Meniul de depanare Deschis până %s Deschis toată ziua imprima editor gematik GmbH\n Friedrichstrasse 136\n 10117 Berlin - Director general: Dr. medical Markus Leyck-Dieken\n Judecătoria de înregistrare: tribunalul districtual Berlin-Charlottenburg\n Număr registru comercial: HRB 96351\n Număr de identificare fiscală: DE241843684 + Director general: Dr. med. Markus Leyck Dieken\n Judecătoria de înregistrare: Judecătoria Berlin-Charlottenburg\n Număr registru comercial: HRB 96351\n Număr de identificare TVA: DE241843684 Responsabil pentru conținut - dr medical Markus Leyck-Dieken + Dr. med. Markus Leyck Dieken a lua legatura Înștiințare - Ne străduim să folosim un limbaj neutru din punct de vedere al genului. Dacă observați vreo eroare, așteptăm cu nerăbdare să primim răspunsuri prin e-mail. + Ne străduim să folosim un limbaj echitabil de gen. Dacă observați vreo eroare, ne-am bucura să vă primim un e-mail. Platforma modernă a Germaniei pentru medicina digitală - scrie mail - site web deschis + Scrie email + Deschide site-ul web Bine ati venit Începeți înregistrarea - debloca + Deblocați Inregistreaza-te - Întrerupe + Anulare Securitate Legal imprima @@ -79,95 +79,95 @@ Detalii Marcați ca răscumpărat Marcați ca nerăscumpărat - forma de dozare - dimensiunea pachetului + Forma de dozare + Dimensiunea pachetului Persoana asigurata Nume de familie abordare Data de naștere - Asigurări de sănătate / Plătitori + Asigurări de sănătate/plătitor stare - numar de asigurare - Persoana care prescrie + Numar de asigurare + Prescriptor Nume de familie Medic specialist Numărul medicului (LANR) instituţie Nume de familie abordare - Numărul sediului comercial - număr de telefon - adresa postala - accident la locul de muncă - ziua accidentului + Numărul plantei + Număr de telefon + Adresa de e-mail + Accident de muncă + Ziua accidentului Firma accidentului sau numărul angajatorului Doriți să ștergeți definitiv această rețetă? - A stinge - Întrerupe + Șterge + Anulare ore de deschidere site-ul web Rambursabil doar astăzi ca plătitor propriu Inregistreaza-te Activați NFC - Vă rugăm să activați funcția NFC a dispozitivului dvs. pentru a vă conecta cu cardul de sănătate. + Vă rugăm să activați funcția NFC a dispozitivului pentru a vă conecta cu cardul de sănătate. Activati Corect Rețete răscumpărate? - Doriți să marcați rețetele ca răscumpărate? + Doriți să marcați rețetele ca fiind răscumpărate? Nu este răscumpărat Răscumpărat - Se deschide la %s + Se deschide la %s oră +49 800 277 377 7 Linia de asistență tehnică Deschideți scanerul pentru rețete - Idei + Setări Suprimați capturile de ecran - Împiedică afișarea unei miniaturi atunci când comutați între aplicații - Permiteți e-rețetei să vă analizeze comportamentul de utilizare în mod anonim? + Împiedică afișarea unei imagini de previzualizare la schimbarea aplicațiilor + Permiteți E-Prescription să vă analizeze comportamentul de utilizare în mod anonim? Informații tehnice Securitatea datelor dumneavoastră de prescripție - Vă rugăm să vă asigurați că persoanele cu care puteți partaja acest dispozitiv și ale căror caracteristici biometrice pot fi stocate pe acest dispozitiv au și acces la rețetele dumneavoastră. + Vă rugăm să vă asigurați că persoanele cu care puteți partaja acest dispozitiv și ale căror caracteristici biometrice pot fi stocate pe acest dispozitiv au și acces la rețetele dvs. trimitere esuata Nu a fost configurat niciun program de e-mail Fara rezultate Nu am găsit niciun rezultat pentru acest termen de căutare. - Licențe Open Source + Licențe open source a lua legatura Apelați linia de asistență tehnică - Participa la sondaj + Participați la sondaj +49 800 277 377 7 Vreau să ajut la îmbunătățirea acestei aplicații - Acestea includ informații despre hardware și software de pe telefon, setări pentru aplicația de prescriere electronică și cantitatea de utilizare, dar niciodată date despre tine sau despre sănătatea ta. - Datele sunt puse la dispoziția gematik GmbH numai de către procesorul de date și sunt șterse cel târziu după 180 de zile. Puteți dezactiva analiza din nou în orice moment în meniul aplicației. - Aceste date ne permit să înțelegem ce funcții sunt utilizate frecvent și să le îmbunătățim. Mai mult, putem estima cât timp trebuie suportată tehnologia mai veche și când putem, de exemplu, să facem obligatorie o versiune mai nouă a sistemului de operare fără a afecta (prea mulți) utilizatori. - îmbunătăți aplicația + Acestea includ informații despre hardware și software despre telefonul dvs., setările aplicației de prescriere electronică și gradul de utilizare, dar niciodată date despre dumneavoastră sau despre sănătatea dumneavoastră. + Datele vor fi puse la dispoziția gematik GmbH numai de către partea care prelucrează datele și vor fi șterse cel târziu după 180 de zile. Puteți dezactiva analiza în orice moment din meniul aplicației. + Aceste date ne permit să înțelegem ce funcții sunt utilizate frecvent și să le îmbunătățim. De asemenea, putem estima cât timp trebuie suportată tehnologia mai veche și când putem, de exemplu, să facem obligatorie o versiune mai nouă a sistemului de operare fără a afecta (prea mulți) utilizatori. + Îmbunătățiți aplicația Analiza anonimă rămâne dezactivată %s Vă mulțumim pentru sprijin! Inregistreaza-te Vă rugăm să vă identificați pentru a descărca rețete. Notă pentru farmacii: Obținem datele de contact și informațiile despre farmacii de la mein-apothekenportal.de al Asociației Germane de Farmacie Ați descoperit o eroare sau doriți să corectați datele? Află mai multe - farmacii + Farmacii Din păcate, nu a funcționat \uD83D\uDE15 Vă rugăm să încercați din nou. Introdu parola Mai departe Accesibilitate - zoom - Vă permite să măriți aplicația prin ciupirea sau desfășurarea degetelor (pentru a mări). + Zoom + Vă permite să măriți aplicația prin pinch-to-zoom. parola Asigurați-vă datele cu o parolă la alegere. parola - Salvați pe computer - arata parola + Salvați + Arata parola Repetați parola Recomandări: %s - scrie mail - Când trimiteți mesajul dvs., vor fi transmise următoarele informații despre hardware-ul și sistemul de operare utilizat: + Scrie e-mail + Când trimiteți mesajul dvs., sunt transmise următoarele informații despre hardware-ul și sistemul de operare utilizat: Valorificați numai pe site Încă nu puteți trimite rețete electronice la această farmacie. Momentan deschis - serviciu de mesagerie + Serviciu de mesagerie Expediere filtru Filtru @@ -175,7 +175,7 @@ Înțeles Parola repetată se potrivește Eroare 20 10 76631 - Certificatul cardului de sănătate este invalid. Ți-a expirat cardul? Vă rugăm să contactați asigurarea dumneavoastră de sănătate. + Certificatul cardului de sănătate este invalid. Poate că cardul tău a expirat? Vă rugăm să contactați compania dumneavoastră de asigurări de sănătate. Încercări de conectare nereușite Au fost detectate %s încercări de conectare nereușite. @@ -183,9 +183,9 @@ Au fost detectate %s încercări de conectare nereușite. Alegeți cel mai bun backup pentru dispozitiv - Aceasta poate fi o amprentă, model de glisare sau similar - jetoane - jeton de acces + Aceasta poate fi o amprentă, un model de glisare sau ceva similar + Jetoane + Jetoane de acces Jetoane SSO Nu există un jeton de acces disponibil nu este disponibil niciun simbol SSO @@ -196,26 +196,26 @@ nicio conexiune la server Vă rugăm să încercați din nou în câteva minute Încărcați din nou - arată jetoane + Afișați jetoane Cum ați dori să securizați aplicația? Înștiințare Nu a fost configurată nicio copie de rezervă a dispozitivului pentru acest dispozitiv - Vă recomandăm să vă protejați suplimentar datele medicale cu securitatea dispozitivului, cum ar fi un cod de acces sau datele biometrice. + Vă recomandăm să vă protejați suplimentar informațiile medicale cu securitatea dispozitivului, cum ar fi un cod sau elemente biometrice. Nu mai afișa această notificare în viitor. Conexiune esuata. O conexiune la rețea nu a putut fi stabilită. Comunicarea cu serverul a eșuat: codul de stare %s . - Nu s-a putut comunica cu serverul: vă rugăm să verificați conexiunea la internet și setările de oră/data. + Comunicarea cu serverul a eșuat: verificați conexiunea la internet și setările de oră/data. avertizare Este posibil ca dispozitivul dvs. să aibă securitate redusă - Acest lucru poate fi cauzat, de exemplu, de dispozitive manipulate sau de un mod de dezvoltator activat. Din motive de securitate, nu vă recomandăm să utilizați aplicația pe dispozitive cu jailbreak. - Recunosc riscul crescut și tot vreau să continui. + Acest lucru poate fi cauzat, de exemplu, de dispozitive manipulate sau atunci când modul dezvoltator este pornit. Vă recomandăm să nu utilizați aplicația pe dispozitive cu jailbreak din motive de securitate. + Recunosc riscul crescut și aș dori totuși să continui. De ce sunt dispozitivele cu acces root un potențial risc de securitate? Află mai multe https://www.bsi.bund.de/SharedDocs/Glossareintraege/DE/R/Rooten.html Numele profilului - Introduceți un nume pentru noul profil. + Vă rugăm să introduceți un nume pentru noul profil. Numele profilului - profiluri + Profiluri Cum să recunoașteți un card de sănătate compatibil NFC Nu este posibil niciun contact prin această aplicație Vă rugăm să utilizați canalele obișnuite pentru a contacta compania de asigurări. @@ -223,12 +223,12 @@ Numai PIN Înregistrare în aplicația e-rețetă Câmpul de nume nu poate fi gol. - Un profil cu numele introdus există deja. + Există deja un profil cu numele pe care l-ați introdus. profil %s selectat culoare de fundal - gri de primăvară - roa soarelui + Gri de primăvară + Roza soarelui Aceasta! Este! Roz! Copac Luna albastră septembrie @@ -236,16 +236,16 @@ Legați împreună Ultima conectare pe %s Ștergeți profilul? - Aceasta va șterge toate datele de profil de pe acest dispozitiv. Rețetele dumneavoastră din rețeaua de sănătate vor rămâne intacte. - A stinge - Întrerupe + Aceasta va șterge toate datele din profilul de pe acest dispozitiv. Rețetele dumneavoastră din rețeaua de sănătate vor fi păstrate. + Șterge + Anulare șterge profilul Doriți să ștergeți ultimul profil. Aplicația necesită cel puțin un profil. Vă rugăm să introduceți un nume pentru noul profil. Eroare 20 10 76831 - Directorul cardurilor de sănătate nu a putut fi accesat. Vă rugăm să încercați din nou. - Puteți găsi informații verificate de experți despre boli, coduri ICD și despre probleme de prevenire și îngrijire pe Portalul Național de Sănătate. - Deschideți Gesund.bund.de + Directorul cardului de sănătate nu a putut fi accesat. Vă rugăm să încercați din nou. + Puteți găsi informații verificate de experți despre boli, coduri ICD și subiecte de prevenire și îngrijire în Portalul Național de Sănătate. + Deschideți healthy.bund.de Am schimbat politica de confidențialitate Aplicația e-rețetă a evoluat. Acest lucru a făcut necesară actualizarea politicii noastre de confidențialitate. Deschideți politica de confidențialitate @@ -254,7 +254,7 @@ Ce se întâmplă dacă folosesc funcția de cameră / citesc rețete cu camera? Nu există rețete noi disponibile - %s rețetă nouă + Noua rețetă %s %s rețete noi @@ -265,18 +265,18 @@ Vizualizați jurnalele de acces Cine a accesat rețetele tale și când? Cheie de acces la serviciul de prescripție medicală - jurnalele de acces - Niciun jurnal de acces + Jurnalele de acces + Fără jurnal de acces Nu există încă jurnalele de acces. Rețeta este în curs de desfășurare și nu poate fi ștearsă Accept Se pare că nu a funcționat - Suntem conștienți că legătura cu cardul de sănătate are capcanele ei. În viitor, înregistrarea ar trebui, prin urmare, să fie posibilă și prin intermediul unei aplicații de asigurări de sănătate deja autentificate. \n\n De asemenea, lucrăm pentru a permite valorificarea digitală a rețetelor fără înregistrare. \n\n Ați observat ceva în timpul acestui proces pe care ați dori să ne împărtășiți? Vă rugăm să ne scrieți, suntem bucuroși să primim feedback foarte critic. + Suntem conștienți că legătura cu cardul de sănătate are capcanele ei. În viitor, înregistrarea ar trebui să fie posibilă și prin intermediul unei aplicații de asigurări de sănătate deja autentificate. \n\n De asemenea, lucrăm pentru a ne asigura că rețetele pot fi răscumpărate digital fără înregistrare. \n\n Ați observat ceva în timpul acestui proces pe care ați dori să ne împărtășiți? Vă rugăm să ne scrieți, am fi bucuroși să primim feedback foarte critic. Sfaturi de conectare Îmbunătățiți puterea conexiunii Dacă este necesar, îndepărtați capacul de protecție. - Dacă dispozitivul vibrează și apoi întrerupe conexiunea, căutați poziția optimă pe o rază mică. - Mutați dispozitivul pe hartă foarte încet. + Dacă dispozitivul vibrează și apoi conexiunea se întrerupe, căutați poziția optimă pe o rază mică. + Mutați dispozitivul foarte încet pe hartă. Așezați dispozitivul direct pe card. Pentru a face acest lucru, așezați cardul de sănătate pe o suprafață plană (de exemplu, o masă). Îmbunătățiți puterea conexiunii @@ -286,60 +286,60 @@ Urmatorul sfat Mai departe Închide - Încercați + Încerca scrie-ne Licență de căutare în farmacii - răscumpăra + Răscumpăra Rețetă scanată Scanat pe %s Marcat ca valorificat pe %s Cum vrei să continui? Ordin Disponibil în curând - Rezervați acum pentru colectare sau primiți-l prin serviciu de curierat sau transport - Salvează pentru o comandă ulterioară + Rezervați acum pentru colectare sau primiți-l prin curier sau transport + Salvează pentru comanda ulterioară Salvați rețetele pe dispozitiv Continuați cu rețeta %s Continuați cu %s rețete - Nu s-a putut conecta cardul de sănătate - Profilul actual este deja conectat la un alt card de sănătate (numărul de asigurări de sănătate %s ). - Cardul tău de sănătate este deja conectat la alt profil. Comutați la profilul %s . - Salvați pe computer - datele de contact si adresa + Conexiunea cardului de sănătate a eșuat + Profilul actual este deja legat de un alt card de sănătate (numărul de asigurări de sănătate %s ). + Cardul dvs. de sănătate este deja legat la alt profil. Accesați profilul %s . + Salvați + Date de contact si adresa a lua legatura - număr de telefon - Vă rugăm să furnizați un număr de telefon pentru contact. + Număr de telefon + Vă rugăm să furnizați un număr de telefon pentru a ne contacta. Adresă de e-mail (opțional) adresă de livrare Primul nume si ultimul nume - Vă rugăm să introduceți un nume și un prenume pentru contact. + Vă rugăm să furnizați un nume și un prenume pentru a ne contacta. Strada și numărul casei - Vă rugăm să introduceți o stradă și un număr de casă pentru a putea fi contactați. + Vă rugăm să furnizați o stradă și un număr de casă pentru a ne contacta. Adresă suplimentară (opțional) - Instrucțiuni de livrare (opțional) - Sunt necesare informații de contact suplimentare + Instructiuni de livrare (optional) + Sunt necesare detalii de contact suplimentare Renunțați la modificări? arunca Pentru căutare, directorul farmaciilor folosește coordonatele geografice care au fost determinate cu ajutorul OpenStreetMap. Mulțumim proiectului pentru acest ajutor. - © OpenStreetMap ( %s ) + © OpenStreetMap ( %s ) https://www.openstreetmap.org/copyright - Confidențialitate și utilizare + Protecția și utilizarea datelor Mai departe Ați primit PIN-ul dvs. într-o scrisoare de la compania dumneavoastră de asigurări de sănătate. Nu s-a primit niciun cod PIN cod PIN - Verificați conexiunea la internet și setarea orei/datei dispozitivului dvs. + Verificați conexiunea la internet a dispozitivului și setările de oră/dată. Pentru a vă autentifica, apăsați pe „Deblocare”. - închis pe dinafară? Vă rugăm să vă verificați acreditările biometrice pe acest dispozitiv. + Închis pe dinafară? Vă rugăm să vă verificați acreditările biometrice pe acest dispozitiv. Aţi uitat parola? Vă rugăm să ștergeți aplicația și apoi să o reinstalați. Puteți afla de ce în %s nostru. zona de ajutor - dimensiunea pachetului și unitate + Dimensiunea pachetului și unitate ingredient activ Cantitatea de ingredient activ - desemnarea lotului + Numele lotului Exp categorie Vaccin @@ -355,35 +355,35 @@ Parola nu este vizibilă biometrie parola - așteptând un răspuns + Astept raspuns Fara retete În prezent, nu aveți rețete rambursabile. A updata Deconectare automată - Din motive de securitate, conexiunea la serverul de rețete se întrerupe după 12 ore. Reconectați-vă pentru a obține rețetele actuale. + Din motive de securitate, conexiunea la serverul de rețete este deconectată după 12 ore. Reconectați-vă pentru a obține rețetele actuale. Conectați - Ai primit o copie pe hârtie? + Ați primit un tipărit pe hârtie? Adăugați rețete în lista dvs. atingând butonul de scanare din colțul din dreapta sus. Scanați imprimarea pe hârtie - Trebuie să fii autentificat pentru a primi rețete automat. + Pentru a primi automat rețete, trebuie să fiți autentificat. Inregistreaza-te Fără rețete răscumpărate Rețetele dvs. răscumpărate sunt afișate aici. Din motive de protecție a datelor, rețetele dumneavoastră vor fi șterse de pe serverul de rețete după 100 de zile. Fără rețete răscumpărate Rețetele dvs. răscumpărate sunt afișate aici. Adăugați rețete prin scanare pentru a începe valorificarea. - managementul dispozitivelor + Gestionarea dispozitivelor Dispozitive conectate Înregistrat de la %s (acest dispozitiv) Înregistrat din %s - Din motive de securitate, conexiunea la serverul de rețete se întrerupe după 12 ore. Pentru a vă reconecta, aveți nevoie de cardul de sănătate și PIN-ul pentru fiecare proces de conectare. + Din motive de securitate, conexiunea la serverul de rețete este deconectată după 12 ore. Pentru a vă reconecta, veți avea nevoie de un card de sănătate și PIN pentru fiecare proces de conectare. cod PIN - Introduceți codul PIN (cardul de sănătate). + Introduceți PIN-ul (cardul de sănătate). Mai departe Inregistreaza-te Dispozitive conectate - indepartati dispozitivul? - Întrerupe - Îndepărtat + Indepartati dispozitivul? + Anulare + Elimina Eliminați acest dispozitiv? Doriți să eliminați %s ? Dacă eliminați %s , conexiunea la serverul de rețete va fi deconectată definitiv în cel mult 12 ore. @@ -396,32 +396,32 @@ wwweg... Fără conexiune internet. Medicamente și pansamente - narcotice - Livrarea medicamentelor prescrise conform § 4 AMVV + Narcotice + Eliberarea medicamentelor eliberate pe bază de rețetă în conformitate cu Secțiunea 4 AMVV Ai nevoie de ajutor? Am adunat câteva sfaturi pentru a rezolva cele mai frecvente probleme. Începeți sfaturi de conectare - debloca + Deblocați card blocat PIN-ul a fost introdus incorect de trei ori. Prin urmare, cardul dvs. a fost blocat din motive de securitate. - debloca cardul + Deblocați cardul Introduceți PUK Cu PIN-ul dvs. ați primit un PUK de 8 cifre de la compania dvs. de asigurări. Alegeți codul PIN nou Puteți alege singur noul număr de identificare personală (PIN) (6 până la 8 cifre). - PIN reținut? - Vă rugăm să notați codul PIN și să-l păstrați într-un loc sigur. - Întrerupe + Ți-ai amintit codul PIN? + Vă rugăm să scrieți codul PIN și să-l păstrați într-un loc sigur. + Anulare Bine Deblocarea nu este posibilă Ați atins numărul maxim de deblocări de card cu acest PUK sau l-ați introdus incorect în mod repetat. Vă rugăm să contactați compania dumneavoastră de asigurări. Puteți folosi un singur PUK pentru până la 10 deblocări. - card deblocat + Card deblocat De ce ai nevoie: - cardul tau de sanatate + Cardul tau de sanatate PUK-ul cardului dumneavoastră de sănătate Mai departe - card de sanatate + Card de sanatate Comandați PIN sau card Inregistreaza-te Cum doriți să vă conectați? @@ -431,70 +431,70 @@ Aplica acum Sau: Conectați-vă cu %s . Aplicația dvs. de asigurări de sănătate - „Numărul dumneavoastră de acces se găsește în colțul din dreapta sus al cardului de sănătate.” + „Puteți găsi numărul de acces în colțul din dreapta sus al cardului de sănătate.” Cardul meu nu are un număr de acces - Mai aveți %s încercări înainte ca cardul dvs. să fie blocat. + Mai aveți %s o încercare înainte ca cardul să fie blocat. - Mai aveți %s încercări înainte ca cardul să fie blocat. + Mai aveți %s încercări înainte ca cardul dvs. să fie blocat. - Pune cardul de sănătate pe spatele telefonului + Puneți cardul de sănătate pe spatele telefonului Următorul proces poate dura până la 30 de secunde. Puneți cardul %s pe spatele telefonului. - în colțul din dreapta sus - în mijlocul de sus - în stânga sus + în zona din dreapta sus + la mijloc în zona superioară + în zona din stânga sus în zona de mijloc din dreapta mijloc - în centrul stânga + în zona de mijloc din stânga în zona din dreapta jos - în centrul inferior - în stânga jos + la mijloc în zona inferioară + în zona din stânga jos Ajutor Trimis acum %s minute Trimis pe %s Trimis chiar acum - Trimis la ora %s + Trimis la %s oră Nu mai este valabil - Conectați-vă cu aplicația - alege asigurarea + Înregistrați-vă cu aplicația + Alegeți asigurarea Nu ați găsit ceea ce căutați? Această listă este în continuă extindere. Înregistrarea cu cardul de sănătate este deja acceptată de fiecare companie de asigurări de sănătate. Feedback din aplicația e-rețetă - Așteptăm cu nerăbdare feedback-ul dvs. Vă rugăm să folosiți spațiul de mai jos și să fiți cât mai precis posibil: + Așteptăm cu nerăbdare feedback-ul dvs. Vă rugăm să folosiți următorul spațiu și să fiți cât mai precis posibil: PUK Închide Ce păcat… - Din păcate, dispozitivul dvs. nu îndeplinește cerințele minime pentru a vă conecta la aplicația e-prescription. Cel puțin Android 7 și un cip NFC sunt necesare pentru autentificarea sigură cu cardul de sănătate. + Din păcate, dispozitivul dvs. nu îndeplinește cerințele minime pentru înregistrarea în aplicația e-rețetă. Pentru autentificarea sigură cu cardul de sănătate, sunt necesare cel puțin Android 7 și un cip NFC. Află mai multe Salvați datele de conectare? - Salvați pe computer + Salvați Nu salva Înștiințare - Din motive de securitate, conexiunea la serverul de rețete se întrerupe după 12 ore. Pentru a vă reconecta, aveți nevoie de un card de sănătate și PIN pentru fiecare proces de conectare. + Din motive de securitate, conexiunea la serverul de rețete este deconectată după 12 ore. Pentru a vă reconecta, veți avea nevoie de un card de sănătate și PIN pentru fiecare proces de conectare. Configurați securitatea biometrică - Salvarea datelor de acces nu este posibilă. Configurați în prealabil securitatea biometrică (de exemplu, amprenta digitală) pe dispozitiv. - Întrerupe - Idei + Nu este posibilă salvarea datelor de acces. În prealabil, configurați securitatea biometrică (de exemplu, amprenta digitală) pe dispozitiv. + Anulare + Setări Înștiințare Accept Securitatea datelor dumneavoastră de prescripție - „Această aplicație folosește cel mai sigur senzor biometric oferit de dispozitiv pentru a vă stoca acreditările într-o zonă securizată a memoriei dispozitivului.” - Securitatea biometrică a datelor de acces vă permite să deschideți această aplicație în viitor fără a fi nevoie să introduceți codul PIN sau un card de sănătate și să vizualizați, să apelați, să răscumpărați sau să ștergeți rețetele. - Vă rugăm să vă asigurați că persoanele cu care puteți partaja acest dispozitiv și ale căror caracteristici biometrice pot fi stocate pe acest dispozitiv au și acces la rețetele dumneavoastră. + „Această aplicație folosește cel mai sigur senzor biometric oferit de dispozitiv pentru a vă securiza acreditările într-o zonă protejată a dispozitivului de stocare.” + Securitatea biometrică a datelor dvs. de acces vă permite să deschideți această aplicație în viitor, să vizualizați, să preluați, să răscumpărați sau să ștergeți rețetele fără un card de sănătate și să introduceți codul PIN. + Vă rugăm să vă asigurați că persoanele cu care puteți partaja acest dispozitiv și ale căror caracteristici biometrice pot fi stocate pe acest dispozitiv au și acces la rețetele dvs. care din pacate nu a functionat - Autentificarea cu aplicația de asigurări de sănătate nu a reușit. + Autentificarea cu aplicația de asigurări de sănătate nu a avut succes. A expirat pe %s Rețeta a fost deja ștearsă de pe server - Vă rugăm să corectați introducerea sau să renunțați la modificări + Vă rugăm să vă corectați intrarea sau să renunțați la modificări Corect - date asigurate + Datele persoanei asigurate Nume de familie Asigurare - numar de asigurare - numărul de acces al cardului + Numar de asigurare + Număr de acces card Inregistreaza-te - De-înregistrați - Salvați pe computer + Deconectați-vă + Salvați Schimbare Editează poza de profil Mai departe @@ -504,14 +504,14 @@ Caută asigurare Conectați-vă la serverul de rețete acum? Conectat cu succes - conexiunea pierdută + conexiune pierdută Conectați-vă la serverul de rețete acum? Fără jetoane - Veți primi un simbol atunci când sunteți conectat la serviciul de prescripție medicală.\n + Veți primi un simbol atunci când sunteți conectat la serviciul de prescripție medicală.\n Comenzi Selectați codul PIN dorit - debloca cardul - Alegeți codul PIN + Deblocați cardul + Selectați PIN Repetați codul PIN Intrările diferă unele de altele. Fara comenzi @@ -519,13 +519,13 @@ Chiar acum La ora %s Coșul de cumpărături este gata - Rețeta a fost adăugată în coșul de cumpărături. Vă rugăm să accesați site-ul farmaciei pentru a finaliza comanda. + Rețeta a fost adăugată în coșul dumneavoastră. Vă rugăm să accesați site-ul farmaciei pentru a finaliza comanda. Deschideți coșul de cumpărături - Arată acest cod de ridicare la farmacie. - Primește codul de ridicare + Arată acest cod de colectare la farmacie. + Cod de preluare primit Mesajul nu poate fi afișat Vă rugăm să contactați farmacia ( %s ). - Afișați linkul coșului + Afișați linkul coșului de cumpărături Afișați codul de ridicare Arată mesajul %s la ora %s @@ -533,7 +533,7 @@ Prezentare generală a comenzii Nou Curs - Ordin + Ordinea Gratuit pentru apelant. Orele de serviciu: Luni - Vineri 8:00 - 20:00, cu excepția sărbătorilor naționale Farmacie Selectați codul PIN dorit @@ -541,40 +541,40 @@ Momentan deschis și lângă mine Filtreaza dupa … incepe cautarea - atribuire directă - farmacii - numarul de telefon (optional) + Atribuire directă + Farmacii + Număr de telefon (opțional) Căutați după nume sau adresă Nu există informații valide despre farmacie Nu au fost găsite informații actuale despre această farmacie. Înregistrarea pentru această farmacie va fi ștearsă. Bine Directorul farmaciilor nu este disponibil - În prezent, nicio informație actuală despre această farmacie nu poate fi preluată. Vă rugăm să vă verificați conexiunea la internet. - Întrerupe + Momentan nu pot fi accesate informații actuale despre această farmacie. Vă rugăm să vă verificați conexiunea la internet. + Anulare Încearcă din nou Salvați Mediul Conectarea nu este posibilă - Se pare că datele dvs. de conectare biometrice s-au schimbat. Vă rugăm să vă înregistrați din nou cu cardul de sănătate. - Întrerupe + Se pare că caracteristicile de conectare biometrice s-au schimbat. Vă rugăm să vă conectați din nou cu cardul de sănătate. + Anulare Inregistreaza-te - profilul 1 + Profil 1 Aproape de mine Rambursabil mai târziu Rambursabil de la %s - îmbunătățiri ale produsului + Îmbunătățiri ale produsului Analiza anonima - Ajutați-ne să îmbunătățim această aplicație. Toate datele de utilizare sunt colectate anonim și sunt folosite doar pentru a îmbunătăți experiența utilizatorului. - securitatea dispozitivului + Ajutați-ne să îmbunătățim această aplicație. Toate datele de utilizare sunt colectate anonim și sunt folosite exclusiv pentru a îmbunătăți experiența utilizatorului. + Securitatea dispozitivului setari personale Accesibilitate - îmbunătățiri ale produsului + Îmbunătățiri ale produsului Rețetă adăugată Reteta deja disponibila A apărut o eroare la import - A stinge + Șterge Rețetă scanată - Posibil înlocuire - Am uitat PIN-ul + Posibilă pregătire pentru înlocuire + PIN uitat %s Rețetă @@ -586,81 +586,80 @@ Am vrea: Îmbunătățiți gradul de utilizare. Detectează erori și blocări. - Toate datele sunt desigur colectate anonim. - Puteți modifica oricând această decizie în setările sistemului. + Toate datele sunt, desigur, colectate anonim. + Puteți modifica oricând această decizie din setările sistemului. Continua Accept Această aplicație folosește cea mai sigură metodă oferită de dispozitivul dvs. - Salvați pe computer + Salvați Alege medicament - nume comercial + Nume comercial da Nu dozare data emiterii Această rețetă va fi răscumpărată pentru dvs. ca parte a unui tratament. Nu este specificat - plata aditionala + Plata aditionala medicament - Note de livrare + Instrucțiuni de depunere Eligibil conform BVG - pregătire alternativă - numele retetei + Pregătire alternativă + Numele rețetei Ambalare - instrucție de lucru + Instructiuni de fabricatie Descriere dat de emis la: ingredient activ - prescris + Prescris A primi Ce este o misiune directă? - În cazul trimiterilor directe, o rețetă de la un cabinet sau un spital este răscumpărată direct la o farmacie. Asigurații nu trebuie să întreprindă nicio măsură și nu pot interveni în procesul de răscumpărare. \n\n Recomandările directe sunt enumerate în aplicația e-rețetă pentru a face tratamentul mai transparent pentru dvs. - taxa de serviciu de urgenta - Uneori este nevoie de grabă. Unele rețete pot fi răscumpărate fără plata suplimentară a unei taxe de serviciu de urgență, cum ar fi noaptea sau de sărbătorile legale. + Cu trimitere directă, o rețetă de la un cabinet sau spital este completată direct la o farmacie. Asigurații nu trebuie să întreprindă nicio măsură și nu pot interveni în procesul de răscumpărare. \n\n Recomandările directe sunt enumerate în aplicația e-rețetă pentru a face tratamentul mai transparent pentru dvs. + Taxa de serviciu de urgenta + Uneori este nevoie de grabă. Unele rețete pot fi completate fără plata suplimentară a unei taxe de serviciu de urgență, de exemplu noaptea sau de sărbători. Medicamente supuse coplății - Scutit de coplata - Cei cu asigurare legală de sănătate trebuie să plătească o coplata de până la zece euro pentru medicamentele eliberate pe bază de rețetă. \n\n Valoarea coplății depinde de prețul medicamentului dumneavoastră. Trebuie să plătiți singur pentru medicamentele care costă mai puțin de 5 EUR.\n Pentru medicamentele care sunt mai scumpe, trebuie să plătiți zece la sută din preț, dar cel puțin 5 euro și maxim 10 euro. \n\n Copiii și tinerii cu vârsta sub 18 ani sunt, în general, scutiți de coplăți. \n\n Dacă costurile dumneavoastră anuale pentru medicamente depășesc limita dumneavoastră financiară, puteți fi scutit de coplată. Discutați cu asigurătorul dvs. de sănătate despre acest lucru. - Sunteți scutit de coplată pentru acest medicament. Asigurarea dumneavoastră de sănătate va acoperi costul medicamentelor. + Scutit de plata suplimentara + Cei cu asigurare legală de sănătate trebuie să plătească o plată suplimentară de până la zece euro pentru medicamentele eliberate pe bază de rețetă. \n\n Valoarea plății suplimentare depinde de prețul medicamentului dumneavoastră. Trebuie să plătiți singur pentru medicamentele care costă mai puțin de 5 EUR.\n Pentru medicamentele care sunt mai scumpe, trebuie să plătiți zece la sută din preț, dar cel puțin 5 euro și maxim 10 euro. \n\n Copiii și tinerii cu vârsta sub 18 ani sunt, în general, scutiți de plata suplimentară. \n\n Dacă costurile anuale pentru medicamente depășesc limita povara financiară, puteți fi scutit de coplăți. Discutați cu compania dumneavoastră de asigurări de sănătate despre acest lucru. + Sunteți scutit de la plata unei coplăți pentru acest medicament. Compania dumneavoastră de asigurări de sănătate va acoperi costul medicamentului. Cât timp este valabilă această rețetă? În această perioadă, vă puteți răscumpăra rețeta în orice farmacie cu o plată suplimentară maximă de 10 EUR. - Posibil înlocuire - Datorită cerințelor legale ale companiei dumneavoastră de asigurări de sănătate, vi se poate oferi o alternativă cu același ingredient activ. \n\n Medicamentele pot arăta și pot fi numite diferit, au prețuri și producători diferiți, dar conțin totuși același ingredient activ. Ingredientul activ în sine și doza sunt deosebit de importante pentru efectul medicamentelor în organism. Pacienții din farmacie primesc adesea un alt medicament decât cel prescris de medic pe rețetă - cu condiția ca medicamentele să fie comparabile. Pot exista motive terapeutice și economice pentru schimbare. + Posibilă pregătire pentru înlocuire + Datorită cerințelor legale din partea companiei dumneavoastră de asigurări de sănătate, vi se poate oferi o alternativă cu același ingredient activ. \n\n Medicamentele pot arăta și pot fi numite diferit, au prețuri și producători diferiți, dar conțin totuși același ingredient activ. Ingredientul activ în sine și doza sunt cruciale pentru efectul medicamentelor în organism. Pacienții primesc adesea un alt medicament la farmacie decât cel prescris de medic - cu condiția ca medicația să fie comparabilă. Pot exista motive terapeutice și economice pentru schimbare. Rețetă scanată - Din motive de securitate, rețetele importate dintr-un tipărit pe hârtie nu trebuie să afișeze date personale sau medicale. \n\n Conectați-vă la această aplicație cu cardul de sănătate sau aplicația de asigurare pentru a vedea toate informațiile conținute în rețetă. + Rețetele importate dintr-o copie hârtie nu pot afișa informații personale sau medicale din motive de securitate. \n\n Conectați-vă la această aplicație cu cardul de sănătate sau aplicația de asigurare pentru a vedea toate informațiile conținute în rețetă. Reteta incorecta - Această rețetă a fost emisă incorect. - Rețetă scanată - taxa de serviciu de urgenta + Această rețetă a fost eliberată incorect. + Taxa de serviciu de urgenta Dozare conform instrucțiunilor scrise telefon - site-ul + site-ul web Poștă Sortarea după distanță nu este posibilă. Bine Introduceți codul PIN actual - PIN introdus incorect + PIN introdus greșit PIN-ul actual al cardului dumneavoastră de sănătate card blocat Deblocați-vă cardul în Setări > Deblocați cardul. Din motive de securitate, introduceți codul PIN actual. - Am uitat PIN-ul + PIN uitat Rețetă incorectă medicament Ceva pare să fi mers prost în timpul creării rețetei. Raportați o eroare? Raport Neconectat Înregistrat cu - card de sanatate + Card de sanatate biometrie Neconectat - Ne interesează opinia dumneavoastră. Vă rugăm să acordați cinci minute pentru a răspunde la sondajul nostru. Vă mulțumesc anticipat. - avertisment de avertizare + Ne interesează opinia dumneavoastră. Vă rugăm să acordați cinci minute pentru a completa sondajul nostru. Vă mulțumesc foarte mult anticipat. + Notă de avertizare Farmacie adăugată la favorite - S-a eliminat Farmacia din Favorite + Farmacie eliminată din favorite Farmaciile mele - Puterea parolei foarte bună + Puterea parolei este foarte bună Operația de scriere nu a reușit PIN-ul nu a putut fi salvat Raport @@ -668,40 +667,40 @@ Regula de acces a fost încălcată Nu aveți permisiunea de a accesa directorul hărților. Atribuiți-vă propriul PIN - Cardul este securizat cu un PIN de la compania dumneavoastră de asigurări de sănătate (PIN de transport), vă rugăm să vă atribuiți propriul PIN. + Cardul este securizat cu un PIN de la compania dumneavoastră de asigurări de sănătate (PIN de transport). Vă rugăm să introduceți propriul PIN. Parola nu a fost găsită Nu există nicio parolă stocată pe cardul dvs. ai fost deconectat Conectați-vă din nou pentru a vă actualiza rețetele. - numărul ingredientului activ + Numărul ingredientului activ potenta si unitate Valorificată acum %s minute Valorificat pe %s Răscumpărat chiar acum Valorificat la ora %s Comenzi - Această rețetă a fost răscumpărată pentru dvs. ca parte a unui tratament. - taxa de serviciu de urgenta - Această rețetă nu poate fi obținută noaptea într-o farmacie fără plata suplimentară a unei taxe de serviciu de urgență. + Această rețetă a fost eliberată ca parte a unui tratament pentru dumneavoastră. + Taxa de serviciu de urgenta + Această rețetă nu poate fi eliberată la o farmacie noaptea fără plata suplimentară a unei taxe de serviciu de urgență. Caută aici - Idei - Partajați locația în setări. + Setări + Partajați locația în Setări. Aproape de mine - Țineți apăsat pentru a edita numele. + Apăsați lung pentru a edita numele. Introduceți noul nume pentru profil. - Trebuie să fiți autentificat pentru a primi rețete digitale de la cabinetul dumneavoastră. + Pentru a primi rețete digital de la cabinetul dumneavoastră, trebuie să fiți autentificat. Primiți rețete digital? - Trageți ecranul în jos pentru a reîmprospăta. + Trageți în jos ecranul pentru a reîmprospăta. Fara retete Adăugați rețete folosind butonul + din colțul din dreapta sus. Inregistreaza-te - arhiva de rețete + Arhiva rețetelor Poate mai târziu Inregistreaza-te Editează poza de profil - arhiva de rețete + Arhiva rețetelor Introdu numele - Salvați pe computer + Salvați Comanda mea Destinatar: in Rețete @@ -710,24 +709,24 @@ Schimbare Ridicați de la farmacie Livrare prin curier - Livrare prin posta + Livrare prin poștă %s Rețete - Valorificarea nu este posibilă + Nu se poate răscumpăra Una sau mai multe rețete nu au putut fi răscumpărate. - Nicio rețetă selectată + Nicio rețetă aleasă Pentru a valorifica rețetele, trebuie selectată cel puțin o rețetă. - Adăugați informații de contact + Adăugați detalii de contact Schimbare - Fără prescripție medicală + Fără rețetă În prezent, nu aveți rețete rambursabile Colectie - curier + băiat de livrare Expediere - alege rețete + Alege rețete Atingeți aici pentru a scana rețetele Apăsați lung pentru a edita numele - Adăugați mai multe profiluri, de exemplu pentru copiii sau părinții dvs - Faceți clic pe afișaj pentru a sări peste indicația afișată. + Adăugați profiluri suplimentare, de exemplu pentru copiii sau părinții dvs + Faceți clic pe afișaj pentru a sări peste sfatul instrument care apare. Cum să răscumpărați? Cum ați dori să primiți medicamentele? Răscumpărați direct @@ -735,10 +734,10 @@ Ordin Rezervați sau primiți-l Gata - cod colectiv - coduri unice + Cod de colectare + Codurile individuale - Aveți %s rețetă. + Ai %s rețetă. Aveți %s rețete. @@ -751,65 +750,65 @@ Înștiințare Această aplicație folosește software de la Google pentru a recunoaște codurile. Află mai multe - Despre scanerul de coduri de rețetă + Informații despre scanerul de coduri de rețetă Ce date contine codul retetei? - Codul rețetei conține doar un identificator al rețetei. Acest lucru permite rețeta să fie găsită pe serviciul de prescripție în rețeaua digitală de sănătate. Codul de prescripție nu conține date despre dumneavoastră sau despre medicamentul dumneavoastră. + Codul rețetei conține doar un identificator pentru rețetă. Aceasta înseamnă că rețeta poate fi găsită pe serviciul de prescripție în rețeaua digitală de sănătate. Codul de prescripție nu conține nicio informație despre dumneavoastră sau despre medicamentul dumneavoastră. Deci nimeni nu poate face nimic singur cu codul rețetei? - Corect. Datele de prescripție trebuie descărcate de la serviciul de prescripție medicală. Acest lucru necesită o autentificare securizată. + Corect. Datele de prescripție trebuie descărcate de la serviciul de prescripție medicală. Pentru aceasta este necesară o autentificare securizată. Cine se poate înregistra pentru serviciul de prescripție medicală? - Înregistrarea la serviciul de prescripție medicală în rețeaua digitală de sănătate este posibilă pentru asigurați, farmacii, cabinete medicale și spitale. + Înregistrarea pentru serviciul de prescripție medicală în rețeaua digitală de sănătate este posibilă pentru asigurați, farmacii, cabinete și spitale. De ce aplicația de prescriere electronică folosește funcțiile Google? - Google oferă funcții care pot fi integrate cu ușurință în aplicații și care sunt dezvoltate și actualizate în mod constant de Google. Acest lucru asigură că funcțiile funcționează pe multe dispozitive finale diferite și pot fi operate în siguranță. Aplicația folosește o funcție pentru a îmbunătăți camera și funcționalitatea de scanare pentru dispozitivele Android (Google ML Kit). - Cum funcționează îmbunătățirea scanării Google ML Kit? + Google oferă funcții care pot fi integrate cu ușurință în aplicații și pe care Google le dezvoltă și le actualizează continuu. Acest lucru asigură că funcțiile funcționează pe multe dispozitive diferite și pot fi operate în siguranță. Aplicația folosește o funcție pentru a îmbunătăți camera și funcționalitatea de scanare pentru dispozitivele Android (Google ML Kit). + Cum funcționează îmbunătățirea scanării cu Google ML Kit? Google ML Kit ajută la optimizarea imaginii surprinse de o cameră astfel încât codurile rețetei să poată fi citite chiar și în condiții de iluminare slabă sau cu modele de camere mai vechi. - Datele despre rețetă sau medicamentele mele vor fi transmise la Google? - Nu. Codul rețetei citit este salvat direct în aplicație. Nu va fi transmis la Google. Datele de prescripție nu sunt stocate în cod, ci doar în rețeaua digitală de sănătate. De acolo sunt trimise în aplicație. Google nu are acces la rețeaua digitală de sănătate. + Datele despre rețetă sau medicamentul meu vor fi partajate cu Google? + Nu. Codul rețetei citit este salvat direct în aplicație. Nu va fi partajat cu Google. Datele de prescripție nu sunt stocate în cod, ci doar în rețeaua digitală de sănătate. De acolo, acestea sunt transmise în aplicație. Google nu are acces la rețeaua digitală de sănătate. Ce date prelucrează Google când folosește ML Kit? - Google are acces numai la informații tehnice despre dispozitivul final utilizat și despre utilizarea generală a funcției suplimentare (de exemplu, rata de eroare, setările camerei) pentru a înregistra acest lucru statistic și a îmbunătăți astfel funcția suplimentară. Când accesați, Google înregistrează temporar adresa IP a dispozitivului dvs. final. Informațiile despre dvs. și conținutul rețetei nu vor fi înregistrate de Google. + Google primește acces doar la informații tehnice despre dispozitivul utilizat și utilizarea generală a funcției suplimentare (de exemplu, rata de eroare, setările camerei) pentru a înregistra acest lucru statistic și pentru a îmbunătăți astfel funcția suplimentară. La accesare, Google înregistrează temporar adresa IP a dispozitivului dvs. Informațiile despre dvs. și conținutul rețetei nu sunt înregistrate de Google. Utilizarea Google ML Kit este voluntară? - Da. Cu toate acestea, ML Kit este încorporat în scanerul de coduri de rețetă din versiunea Android a aplicației e-prescription. Dacă utilizați scanerul de coduri de rețetă pe un dispozitiv Android, funcția ML Kit este întotdeauna utilizată. Cu toate acestea, puteți face fără a utiliza scanerul de coduri de rețetă. Rețetele dumneavoastră pot fi încărcate în aplicație și dacă vă înregistrați în rețeaua digitală de sănătate cu cardul electronic de sănătate sau prin aplicația de asigurări de sănătate. + Da. Cu toate acestea, ML Kit este încorporat în scanerul de coduri de rețetă din versiunea Android a aplicației e-prescription. Dacă utilizați scanerul de coduri de rețetă pe un dispozitiv Android, funcția ML Kit este întotdeauna utilizată. Cu toate acestea, puteți evita utilizarea scanerului de coduri de rețetă. Rețetele dumneavoastră pot fi încărcate în aplicație și dacă vă conectați la rețeaua digitală de sănătate cu cardul electronic de sănătate sau prin aplicația de asigurări de sănătate. Pot să văd cine mi-a văzut rețetele? Da. Toate accesul la datele dvs. este complet înregistrat în rețeaua digitală de sănătate. În aplicația de rețetă electronică puteți vedea cine v-a accesat datele. - Pe cine pot contacta dacă am întrebări despre aplicație sau rețetă electronică? - Puteți găsi informații detaliate în declarația de protecție a datelor. + Unde pot contacta dacă am întrebări despre aplicație sau rețetă electronică? + Informații detaliate pot fi găsite în declarația de protecție a datelor. Numărul de pachete prescris Fara retete Pentru aceasta aveți nevoie de rețete rambursabile. - alege asigurarea + Alegeți asigurarea Caută asigurare - Întrerupe + Anulare Pentru ce ați dori să aplicați? Pentru această aplicație aveți nevoie de un card și PIN-ul asociat. Cum ați dori să contactați compania dvs. de asigurări? Compania dumneavoastră de asigurări vă oferă următoarele opțiuni de contact - Compania dumneavoastră de asigurări vă oferă următoarele opțiuni de contact + Compania dumneavoastră de asigurări vă oferă următoarea opțiune de contact Închide PIN introdus incorect. Numărul de acces a fost introdus incorect PUK a fost introdus incorect. chitanțe de cheltuieli - Afișați chitanțele de cheltuieli + Vedeți chitanțele de cost chitanțe de cheltuieli Pentru a primi chitanțe de cheltuieli, trebuie să fiți conectat la server. Conectați - Fără chitanțe de cheltuieli + Fără chitanțe de cost Dezactivați - Întrerupe - dezactivați funcția - Aceasta va șterge toate chitanțele de pe acest dispozitiv și de pe server. - Primiți chitanțe de cheltuieli + Anulare + Dezactivați funcția + Aceasta va șterge toate chitanțele de cheltuieli de pe acest dispozitiv și de pe server. + Primiți chitanțe de cost Încasările dvs. de cost sunt salvate și pe serverul de rețete. - A primi + Primit Total: %s %s Alege Despică - A stinge - A stinge + Șterge + Șterge Trimite %s € pretul total - Sfat: trimiteți chitanțele de cheltuieli prin aplicația de asigurare - Trimiteți cu ușurință chitanțele de cost prin aplicația companiei dvs. de asigurări. În pasul următor, selectați această aplicație și apăsați Partajare. + Sfat: trimiteți chitanțele de cost prin aplicația de asigurare + Trimiteți cu ușurință chitanțele de cost prin aplicația companiei dvs. de asigurări. În pasul următor, selectați această aplicație și apăsați pe share. Practică Farmacie Data @@ -817,44 +816,79 @@ ID de droguri Eliberat pentru KVNR: %s - Data nașterii: %s + Născut pe: %s Bine - Cum depuneți chitanțele? - Transferați direct în aplicația companiei dumneavoastră de asigurări/oficiului de ajutor. Pentru a face acest lucru, selectați aplicația de pe pagina următoare. + Cum depuneți documentele justificative? + Transferați direct în aplicația biroului dumneavoastră de asigurări/beneficii. Pentru a face acest lucru, selectați aplicația de pe pagina următoare. sau - Salvați fișierul și apoi importați-l în portalul de asigurări/ajutor. + Salvați fișierul și apoi importați-l în portalul de asigurări/beneficii. Articol: %s Număr: %s TVA: %s %% Preț brut în EUR: %s Taxe suplimentare - taxa de serviciu de urgenta + Taxa de serviciu de urgenta Taxa BTM - T taxa de prescriptie medicala - costurile de achiziție - serviciu de mesagerie + Taxa de rețetă T + Costurile de achiziție + Serviciu de mesagerie Total în EUR: %s taxă Ștergeți cu adevărat? Fișierul va fi șters de pe dispozitiv și de pe server. - A stinge + Șterge Postat Cod poștal Locație - Vă rugăm să introduceți codul poștal pentru a ne contacta. - Vă rugăm să introduceți locul de reședință când ne contactați. + Vă rugăm să furnizați codul poștal pentru a ne contacta. + Vă rugăm să indicați locul de reședință pentru a ne contacta. Va fi răscumpărat pentru tine A fost răscumpărat pentru tine Trebuie să fiți autentificat pentru a utiliza acest serviciu. - aplicația de asigurare - card de sanatate + Aplicația de asigurare + Card de sanatate PIN asociat este necesar + Poate fi răscumpărat doar mâine ca plătitor propriu + Au mai rămas doar %s zile pentru a valorifica ca plătitor propriu + \nÎncă poate fi răscumpărat ca plătitor propriu pentru %s zile\n + Valabil numai pentru %s zile + \nValabil pentru %s zile rămase\n + Valabil doar maine + Se aplică taxe + Ia asigurare + Rețetele au fost transferate cu succes. + Rețeta nu poate fi procesată. Vă rugăm să încercați din nou. Poate fi necesar să alegeți o altă farmacie. + Rețeta nu poate fi procesată. Farmacia raportează o eroare necunoscută. Dacă este necesar, încercați o altă farmacie. + Rețeta a fost respinsă de farmacie. Rețeta poate fi invalidă sau adresa dumneavoastră de livrare sau informațiile de contact pot fi invalide. + Nu se poate valorifica, vă rugăm să vă verificați conexiunea la internet. + Rețeta a fost transferată cu succes. Cu toate acestea, farmacia raportează o eroare de procesare. Vă rugăm să contactați farmacia. + Rețeta a fost respinsă de farmacie. Rețeta a fost deja răscumpărată. + Rețeta a fost respinsă de farmacie. Rețeta a fost ștearsă. + Rețeta nu a putut fi transferată. Vă rugăm să vă verificați conexiunea la internet și să încercați din nou. + Una sau mai multe rețete nu au putut fi transferate. + Eroare la trimitere + Expediat cu succes! + Eroare la farmacie + Eroare la farmacie + Contactati farmacia + Rețetă deja răscumpărată + Rețeta a fost ștearsă + Fara Internet Pentru a primi jurnalele de acces, trebuie să fiți conectat la server. - Puteți completa rețeta la o farmacie în această perioadă, dar va trebui să plătiți singur prețul de achiziție al medicamentului. Ca alternativă, puteți cere cabinetului dumneavoastră să fie reemisă rețeta. + Puteți completa rețeta la o farmacie în această perioadă, dar va trebui să plătiți singur prețul de achiziție al medicamentului. Ca alternativă, puteți solicita cabinetului dumneavoastră să fie reemisă rețeta. Gata Solicitați corectare La farmacie În aplicație Scanează acest cod la farmacie. Solicitare de corectare a facturării + medicament + Vă rugăm să introduceți cel puțin 1 caracter. + Sau. Încercați aplicația în modul demonstrativ + Modul demonstrativ + Modul demonstrativ + Utilizați modul demo + Modul demonstrativ activat + Termină aici + Activați modul demo diff --git a/android/src/main/res/values-ru/strings.xml b/app/features/src/main/res/values-ru/strings.xml similarity index 69% rename from android/src/main/res/values-ru/strings.xml rename to app/features/src/main/res/values-ru/strings.xml index 1176427c..6dd27f33 100644 --- a/android/src/main/res/values-ru/strings.xml +++ b/app/features/src/main/res/values-ru/strings.xml @@ -16,27 +16,27 @@ Код рецепта недействителен Этот код рецепта уже отсканирован - Распознан %s рецепт - Распознано %s рецепта - Распознано %s рецептов - Распознано %s рецептов + Рецепт %s распознан + + + Обнаружено %s рецептов Отмена Подсветка камеры - Прервать сканирование кодов рецептов? - Прервать сканирование - Продолжить - Давайте начнем + Прервать сканирование? + ХОРОШО + Не прерывать + Вперед Вам потребуется: Введите номер доступа к карте Ввести PIN-код Попробовать снова Не удалось подключиться к серверу. - У вас осталась еще %s попытка, прежде чем ваша карточка будет заблокирована. - У вас осталось еще %s попытки, прежде чем ваша карточка будет заблокирована. - У вас осталась еще %s попыток, прежде чем ваша карточка будет заблокирована. - У вас осталось еще %s попыток, прежде чем ваша карточка будет заблокирована. + У вас есть еще %s попытка, прежде чем ваша карта будет заблокирована. + + + У вас есть еще %s попыток, прежде чем ваша карта будет заблокирована. Номер доступа указан в верхнем правом углу медицинской карточки. Отмена @@ -71,7 +71,7 @@ Добро пожаловать Начать процедуру входа Разблокировать - Войти + регистр Отмена Безопасность Юридическая информация @@ -82,14 +82,14 @@ Отметить как выкупленный Отметить как не выкупленный Форма выпуска - стандартный размер + Размер упаковки Застрахованное лицо Фамилия Адрес Дата рождения Организация медицинского страхования / плательщик Статус - Страховой номер + Номер застрахованного лица Лицо, выписавшее рецепт Фамилия Врач @@ -99,7 +99,7 @@ Адрес Номер учреждения Номер телефона - Электронная почта + Адрес электронной почты Производственная травма Дата происшествия Номер предприятия, на котором произошел несчастный случай, или номер работодателя @@ -109,7 +109,7 @@ Часы работы Веб-сайт Можно выкупить в качестве самостоятельного плательщика только сегодня - Войти + регистр Активировать NFC Активируйте функцию NFC на своем устройстве, чтобы войти в систему со своей медицинской карточкой. Активировать @@ -117,7 +117,7 @@ Рецепты выкуплены? Отметить рецепты как выкупленные? Не выкуплены - Выкуплены + Выкуплен Откроется в %s +49 800 277 377 7 Техническая горячая линия @@ -125,10 +125,10 @@ Настройки Отключить скриншоты Скрывать предпросмотр при смене приложения - Разрешаете ли вы приложению E-Rezept анонимно анализировать поведение пользователя? + Разрешаете ли вы E-Prescription анонимно анализировать ваше поведение при использовании? Техническая информация Защита данных ваших рецептов - Примите во внимание: если этим устройством вместе с вами пользуются другие люди, которые сохранили свои биометрические параметры на устройстве или знают его PIN-код, графический ключ или пароль, они могут получить доступ к вашим рецептам. + Примите во внимание: если этим устройством вместе с вами пользуются другие люди, которые сохранили свои биометрические параметры на устройстве, они могут получить доступ к вашим рецептам. Не удалось отправить Приложение электронной почты не настроено Результаты не найдены @@ -141,17 +141,17 @@ Я хочу помочь в работе по улучшению этого приложения Эти данные включают в себя информацию об аппаратном и программном обеспечении вашего телефона, настройках приложения E-Rezept и объеме использования, но никогда не включают информацию о вас или вашем здоровье. Обработчики данных предоставляют информацию только компании gematik GmbH и удаляют ее не позднее чем через 180 дней. Вы можете в любое время деактивировать анализ в меню приложения. - Эти данные позволяют нам понять, какие функции часто используются, и оптимизировать их. Кроме того, мы оцениваем, на протяжении какого времени требуется поддержка старого оборудования и когда, например, мы можем включить в системные требования более новую версию операционной системы, чтобы изменения затронули как можно меньше пользователей. + Эти данные позволяют нам понять, какие функции часто используются, и улучшить их. Мы также можем оценить, как долго необходимо поддерживать старую технологию и когда мы сможем, например, сделать обязательную новую версию операционной системы, не затрагивая (слишком много) пользователей. Улучшить приложение Анонимный анализ остается деактивирован %s Спасибо за вашу поддержку! - Войти - Пройдите идентификацию для загрузки рецептов. - Информация для аптек: это приложение получает контактные данные и информацию об аптеках с сайта mein-apothekenportal.de Немецкой ассоциации фармацевтов Deutscher Apothekerverband e.V. Вы обнаружили ошибку или хотите исправить данные? + регистр + Пожалуйста, назовите себя, чтобы скачать рецепты. + Примечание для аптек: Контактные данные и информацию об аптеках мы получаем на сайте mein-apothekenportal.de Немецкой ассоциации аптек. Вы обнаружили ошибку или хотите исправить данные? Узнать больше - Аптеки + аптеки К сожалению, выполнить не удалось \uD83D\uDE15 - Попробуйте еще раз. + Пожалуйста, попробуйте еще раз. Ввести пароль Далее Вспомогательные инструменты @@ -177,13 +177,13 @@ Понятно Пароли совпадают Ошибка 20 10 76631 - Сертификат вашей медицинской карточки недействителен. Может быть, срок действия вашей карточки истек? Обратитесь в свою организацию медицинского страхования. - Безуспешные попытки входа + Сертификат вашей медицинской карты недействителен. Может быть, срок действия вашей карты истек? Пожалуйста, свяжитесь со своей медицинской страховой компанией. + Неудачные попытки входа - Зафиксирована %s безуспешная попытка входа. - Зафиксировано %s безуспешных попытки входа. - Зафиксировано %s безуспешных попыток входа. - Зафиксировано %s безуспешных попыток входа. + Обнаружено %s неудачных попыток входа в систему. + + + Обнаружено %s неудачных попыток входа в систему. Выбрать наилучшую функцию защиты устройства Это может быть отпечаток пальца, графический ключ и т.п. @@ -194,7 +194,7 @@ SSO-токен недоступен копирование в буфер обмена выполнено Нажмите, чтобы скопировать токен в буфер обмена - действительно только сегодня + Действует только сегодня Разрешить Отсутствует соединение с сервером Попробуйте снова через несколько минут @@ -210,7 +210,7 @@ Не удалось связаться с сервером: проверьте подключение к Интернету и настройки времени/даты. Предупреждение Ваше устройство может иметь пониженную безопасность - Это может быть вызвано, например, управляемыми устройствами или активированным режимом разработчика. Из соображений безопасности мы не рекомендуем использовать приложение на взломанных устройствах. + Это может быть вызвано, например, манипуляциями с устройствами или включением режима разработчика. Мы рекомендуем не использовать приложение на взломанных устройствах по соображениям безопасности. Я понимаю повышенный уровень риска и, несмотря на это, хочу продолжить. Почему устройства с корневым доступом потенциально небезопасны? Узнать больше @@ -236,10 +236,10 @@ Дерево Синяя луна сентябрь Вход не выполнен - Соединение установлено + Привязка выполнена Дата последнего соединения %s Удалить профиль? - Все данные профиля на этом устройстве будут удалены. Ваши рецепты в сети здравоохранения сохранятся. + Это приведет к удалению всех данных из профиля на этом устройстве. Ваши рецепты в сети здравоохранения будут сохранены. Удалить Отмена Удалить профиль @@ -257,25 +257,25 @@ Что происходит, когда я использую камеру / считываю рецепты с помощью камеры? Новые рецепты недоступны - новый рецепт - новые рецепты - новые рецепты - новые рецепты + новый рецепт %s + + + %s новых рецептов Можно выкупить В искуплении Выкуплен Неизвестно Показать протоколы доступа - Здесь вы можете увидеть, кто обращался к вашим рецептам - Это ключ для доступа к службе рецептов + Кто и когда обращался к вашим рецептам? + Ключ для доступа к службе рецептов Протоколы доступа Нет протоколов доступа Протоколов доступа еще нет. Рецепт в настоящее время обрабатывается и не может быть удален - Принять + Принимать Видимо, что-то пошло не так - Мы знаем, что при установлении соединения с медицинской карточкой могут возникать сложности. Поэтому в будущем планируем создать возможность входа в систему через приложение организации медицинского страхования, в котором аутентификация уже пройдена.\n\nМы работаем также над тем, чтобы рецепты можно было выкупать в электронной форме и без входа в систему.\n\nВы заметили что-нибудь во время этого процесса, о чем хотели бы сообщить нам? Мы будем рады вашим отзывам, даже очень критическим. + Мы знаем, что подключение к медицинской карте имеет свои подводные камни. В будущем регистрация также станет возможной через уже проверенное приложение медицинского страхования. \n\n Мы также работаем над тем, чтобы рецепты можно было выкупать в цифровом виде без регистрации. \n\n Заметили ли вы что-нибудь во время этого процесса, чем хотели бы поделиться с нами? Напишите нам, мы также будем рады получить критический отзыв. Советы по улучшению качества соединения Улучшите качество соединения Снимите чехол (при наличии). @@ -304,9 +304,9 @@ Сохранить, чтобы заказать позднее Сохранить рецепты на устройстве - Продлжить с %s рецептом - Продолжить с %s рецептами - Продолжить с %s рецептами + Продолжить рецепт %s + + Продолжить с %s рецептами Ошибка привязки медицинской карточки @@ -341,18 +341,18 @@ Блокировка? Проверьте свои биометрические данные доступа на этом устройстве. Забыли пароль? Удалите приложение и затем установите его заново. Причины мы объясняем в %s. Раздел справки - размер упаковки и единица измерения + Размер упаковки и единица измерения Действующее вещество Количество действующего вещества Обозначение партии Годен до Категория Вакцина - Принять + Принимать Отменить Указание Помогите нам сделать это приложение лучше - Установить собственный пароль + Ввести пароль Пароль должен состоять как минимум из восьми символов Надежность пароля недостаточная Надежность пароля достаточная @@ -371,7 +371,7 @@ Добавьте рецепты в свой список, нажав кнопку сканирования в правом верхнем углу. Отсканировать распечатку Чтобы получать рецепты автоматически, необходимо войти в систему. - Войти + регистр Нет выкупленных рецептов Здесь отображаются ваши выкупленные рецепты. В целях защиты данных ваши рецепты удаляются с сервера рецептов через 100 дней. Нет выкупленных рецептов @@ -380,11 +380,11 @@ Привязанные устройства Дата регистрации %s (данное устройство) Дата регистрации %s - В целях безопасности соединение с сервером предписаний прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. + В целях безопасности соединение с сервером рецептов прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. PIN-код Введите PIN-код (карточки здоровья) Далее - Авторизоваться + регистр Привязанные устройства Удалить устройство? Отмена @@ -407,28 +407,28 @@ Мы подобрали ряд советов по решению наиболее часто встречающихся проблем. Показать советы по привязке Разблокировать - Карточка заблокирована + карта заблокирована PIN-код был введен неверно три раза, поэтому ваша карточка заблокирована в целях безопасности. - Разблокировать карточку - Ввести PUK-код - Вместе с PIN-кодом вы получили от своей страховой организации 8-значный PUK-код. + разблокировать карту + Введите PUK-код + Вместе с PIN-кодом вы получили 8-значный PUK-код от вашей страховой компании. Выбрать новый PIN-код - Новый индивидуальный идентификационный номер (PIN-код) вы можете выбрать самостоятельно (от 6 до 8 символов). - Запомнили PIN-код? + Вы можете самостоятельно выбрать новый персональный идентификационный номер (PIN) (от 6 до 8 цифр). + PIN-код запомнился? Запишите свой PIN-код и сохраните записку в надежном месте. Отмена OK Деблокировка невозможна Вы достигли максимального количества операций деблокировки с помощью этого PUK-кода либо повторно ввели неправильный код. Обратитесь в свою страховую организацию. - Вы можете использовать PUK-код максимум для 10 операций деблокировки. - Карточка разблокирована + Вы можете использовать один PUK-код для 10 разблокировок. + карта разблокирована Вам потребуется: Ваша медицинская карточка PUK-код вашей медицинской карточки Далее - Медицинская карточка + страховой полис Заказать PIN-код или карту - Войти + регистр Как вы хотите войти в систему? Медицинская карточка с поддержкой NFC PIN-код медицинской карточки @@ -439,10 +439,10 @@ \"Номер доступа находится на вашей медицинской карточке в правом верхнем углу\". У моей карточки нет номера доступа - У вас осталась еще %s попытка, прежде чем ваша карточка будет заблокирована. - У вас осталось еще %s попытки, прежде чем ваша карточка будет заблокирована. - У вас осталось еще %s попыток, прежде чем ваша карточка будет заблокирована. - У вас осталось еще %s попыток, прежде чем ваша карточка будет заблокирована. + У вас есть еще %s попытка, прежде чем ваша карта будет заблокирована. + + + У вас есть еще %s попыток, прежде чем ваша карта будет заблокирована. Приложите медицинскую карточку к обратной стороне телефона Следующая процедура может занять до 30 секунд. @@ -464,7 +464,7 @@ Недействительно Войти с помощью приложения Выбрать страховую организацию - Не удалось найти? Список постоянно дополняется. Функцию входа с помощью медицинской карточки поддерживают уже все организации медицинского страхования. + Не нашли то, что искали? Этот список постоянно расширяется. Регистрация с помощью медицинской карты уже поддерживается каждой медицинской страховой компанией. Обратная связь от приложения E-Rezept Мы будем рады вашим откликам. Введите в поле, расположенное ниже, максимально точные формулировки: PUK-код @@ -476,62 +476,62 @@ Сохранить Не сохранять Указание - В целях безопасности соединение с сервером предписаний прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. + В целях безопасности соединение с сервером рецептов прерывается через 12 часов. Для повторного подключения вам потребуется карта здоровья и PIN-код для каждого процесса подключения. Настроить биометрическую защиту Сохранение данных доступа невозможно. Сначала настройте на своем устройстве биометрическую защиту (например, с помощью отпечатка пальца). Отмена Настройки Указание - Принять + Принимать Защита данных ваших рецептов \"Это приложение использует самый надежный биометрический датчик, имеющийся на вашем устройстве, для обеспечения безопасности ваших данных доступа в защищенном разделе памяти устройства. \" Биометрическая защита ваших данных доступа позволяет в будущем без помощи медицинской карточки и ввода PIN-кода открывать это приложение, а также просматривать, запрашивать, выкупать и удалять рецепты. - Примите во внимание: если этим устройством вместе с вами пользуются другие люди, которые сохранили свои биометрические параметры на устройстве или знают его PIN-код, графический ключ или пароль, они могут получить доступ к вашим рецептам. + Примите во внимание: если этим устройством вместе с вами пользуются другие люди, которые сохранили свои биометрические параметры на устройстве, они могут получить доступ к вашим рецептам. К сожалению, выполнить не удалось - Аутентификация с помощью приложения организации медицинского страхования не пройдена. - Срок истек %s - Рецепт уже удален с сервера - Пожалуйста, исправьте введенные данные или отмените изменения + Аутентификация в приложении медицинского страхования не удалась. + Срок действия истек %s + Рецепт уже удален с сервера. + Пожалуйста, исправьте введенные данные или отмените изменения. Исправить Данные застрахованного лица Фамилия Страховая организация - Страховой номер + Номер застрахованного лица Номер доступа к карте - Войти + регистр Выйти Сохранить Изменять - Изменить изображение профиля + Редактировать изображение профиля Далее Сервер не отвечает Повторите попытку позже. Попробовать снова - Ищите страховку + Поиск страховки Подключиться к серверу рецептов сейчас? Вы успешно вошли в систему соединение потеряно Подключиться к серверу рецептов сейчас? - Нет токенов - Вы получите токен, когда войдете в службу рецептов.\n + Нет жетонов + Вы получите токен, когда войдете в систему рецептурной службы.\n заказы Выберите нужный PIN-код - Разблокировать карточку + разблокировать карту Выберите PIN-код - Повторите PIN-код - Введенные данные не совпадают. + Повторить PIN-код + Записи отличаются друг от друга. Нет заказов У вас пока нет заказов. Только что В %s часов - Корзина готова - Рецепт добавлен в корзину. Пожалуйста, перейдите на сайт аптеки, чтобы завершить заказ. + Корзина готова. + Рецепт добавлен в вашу корзину. Пожалуйста, перейдите на сайт аптеки, чтобы оформить заказ. Открыть корзину - Покажите этот код самовывоза в аптеке. - Получен код самовывоза + Покажите этот код коллекции в аптеке. + Получить код самовывоза Сообщение не может быть отображено Пожалуйста, свяжитесь с вашей аптекой ( %s ). - Показать ссылку корзины + Показать ссылку на корзину Показать код самовывоза Показать сообщение %s в %s часов @@ -540,49 +540,49 @@ Новый Курс Заказ - Бесплатно для звонящего. Время работы: пн-пт с 8:00 до 20:00 кроме государственных праздников + Бесплатно для звонящего. Время обслуживания: пн-пт с 8:00 до 20:00, кроме национальных праздников. Аптека Выберите нужный PIN-код Желаемый PIN-код сохранен - В настоящее время открыто и рядом со мной + Сейчас открыто и рядом со мной Сортировать по … начать поиск - прямое назначение + Прямое назначение аптеки - Телефонный номер (не обязательно) + Номер телефона (необязательно) Поиск по имени или адресу - Нет достоверной информации об аптеке - Актуальной информации об этой аптеке не найдено. Запись об этой аптеке будет удалена. + Нет достоверной информации об аптеке. + Никакой актуальной информации об этой аптеке не найдено. Запись об этой аптеке будет удалена. OK - Справочник аптек недоступен - В настоящее время невозможно получить текущую информацию об этой аптеке. Пожалуйста, проверьте подключение к интернету. + Справочник аптек недоступен. + В настоящее время актуальная информация об этой аптеке недоступна. Пожалуйста, проверьте ваше интернет-соединение. Отмена Попробовать снова Сохранить окружающую среду Вход невозможен - Похоже, ваши биометрические данные для входа изменились. Пожалуйста, зарегистрируйтесь снова с вашей картой здоровья. + Похоже, ваши биометрические характеристики входа изменились. Пожалуйста, войдите еще раз, используя свою медицинскую карту. Отмена - Войти - профиль 1 + регистр + Профиль 1 Близко ко мне - Можно использовать позже - Можно получить от %s - улучшения продукта + Можно погасить позже + Можно погасить у %s + Улучшения продукта Анонимный анализ - Помогите нам сделать это приложение лучше. Все пользовательские данные собираются анонимно и используются только для улучшения пользовательского опыта. - безопасность устройства + Помогите нам сделать это приложение лучше. Все данные об использовании собираются анонимно и используются исключительно для улучшения пользовательского опыта. + Безопасность устройства персональные настройки Вспомогательные инструменты - улучшения продукта + Улучшения продукта Добавлен рецепт Рецепт уже доступен Произошла ошибка при импорте Удалить Отсканированный рецепт Возможна замена препарата - Забыли PIN-код + Забыт PIN-код - %s рецепт + %s Рецепт %s Рецепты @@ -591,161 +591,160 @@ Политика конфиденциальности Условия эксплуатации Мы хотели бы: - Улучшить удобство использования. + Улучшите удобство использования. Обнаружение ошибок и сбоев. - Все данные, разумеется, собираются анонимно. + Все данные, конечно, собираются анонимно. Вы можете в любое время изменить это решение в системных настройках. Продолжить - Принять - Это приложение использует самый безопасный метод, предоставляемый вашим устройством. + Принимать + Это приложение использует самый безопасный метод, предусмотренный вашим устройством. Сохранить Выбирать Препарат - торговое название + Торговое название Да - нет + Нет дозировка - Дата выпуска + Дата выдачи Этот рецепт будет выкуплен для вас как часть лечения. Нет данных - дополнительный платеж + Дополнительный платеж Препарат - Накладные - Соответствует требованиям BVG - альтернативная подготовка - имя формулы + Инструкции по отправке + Соответствует критериям BVG + Альтернативная подготовка + Название рецепта Упаковка - инструкция по изготовлению - описание + Инструкция по изготовлению + Описание данный выпущено: Действующее вещество - предписанный + Прописано Получать - Что такое прямое назначение? - В случае прямых направлений рецепт из практики или больницы выкупается непосредственно в аптеке. Застрахованные не обязаны предпринимать никаких действий и не могут вмешиваться в процесс выкупа. \n\n Прямые направления перечислены в приложении электронных рецептов, чтобы сделать ваше лечение более прозрачным для вас. + Что такое прямое задание? + При прямом направлении рецепт из практики или больницы выписывается непосредственно в аптеке. Застрахованные лица не обязаны предпринимать никаких действий и не могут вмешиваться в процесс погашения. \n\n Прямые направления указаны в приложении электронного рецепта, чтобы сделать ваше лечение более прозрачным для вас. Плата за аварийное обслуживание Иногда возникает необходимость в спешке. Некоторые рецепты могут быть заполнены без дополнительной оплаты за услуги неотложной помощи, например, в ночное время или в праздничные дни. - Препараты, подлежащие доплате - Освобожден от доплаты - Те, у кого есть государственная медицинская страховка, должны внести доплату в размере до десяти евро за лекарства, отпускаемые по рецепту. \n\n Размер доплаты зависит от стоимости вашего лекарства. Вы должны платить за лекарства стоимостью менее 5 евро самостоятельно.\n За более дорогие лекарства вы должны заплатить десять процентов от цены, но не менее 5 евро и не более 10 евро. \n\n Дети и молодые люди в возрасте до 18 лет, как правило, освобождаются от доплаты. \n\n Если ваши ежегодные расходы на лекарства превышают ваш финансовый лимит, вы можете быть освобождены от доплаты. Поговорите об этом со своей страховой компанией. - Вы освобождаетесь от доплаты за этот препарат. Ваша медицинская страховка покроет стоимость лекарства. - Как долго действует этот рецепт? + Лекарства, подлежащие доплате + Освобождены от дополнительной оплаты + Те, у кого есть государственная медицинская страховка, должны заплатить дополнительную плату в размере до десяти евро за лекарства, отпускаемые по рецепту. \n\n Размер доплаты зависит от цены вашего лекарства. За лекарства стоимостью менее 5 евро вам придется платить самостоятельно.\n За более дорогие лекарства придется заплатить десять процентов от цены, но минимум 5 евро и максимум 10 евро. \n\n Дети и молодые люди до 18 лет, как правило, освобождаются от дополнительной оплаты. \n\n Если ваши годовые расходы на лекарства превышают лимит вашего финансового бремени, вы можете быть освобождены от доплаты. Поговорите об этом со своей медицинской страховой компанией. + Вы освобождаетесь от доплаты за этот препарат. Ваша медицинская страховая компания покроет стоимость лекарства. + Как долго действителен этот рецепт? В течение этого периода вы можете выкупить рецепт в любой аптеке с максимальной доплатой в размере 10 евро. Возможна замена препарата - В соответствии с юридическими требованиями вашей медицинской страховой компании вам может быть предоставлена альтернатива с тем же активным ингредиентом. \n\n Лекарства могут выглядеть и называться по-разному, иметь разную цену и производителя, но при этом содержать одно и то же действующее вещество. Сам активный ингредиент и дозировка особенно важны для действия лекарств в организме. Пациенты в аптеке часто получают другой препарат, чем тот, который выписал врач по рецепту, при условии, что препараты сопоставимы. Для изменения могут быть терапевтические и экономические причины. + В соответствии с юридическими требованиями вашей медицинской страховой компании вам может быть предоставлена ​​альтернатива с тем же активным ингредиентом. \n\n Лекарства могут выглядеть и называться по-разному, иметь разные цены и производителей, но при этом содержать одно и то же действующее вещество. Сам активный ингредиент и дозировка имеют решающее значение для воздействия лекарства на организм. Пациенты часто получают в аптеке лекарство, отличное от того, которое прописал врач, при условии, что лекарство сопоставимо. Для изменения могут быть терапевтические и экономические причины. Отсканированный рецепт - Из соображений безопасности рецепты, импортированные из бумажной распечатки, не должны содержать никаких личных или медицинских данных. \n\n Войдите в это приложение с медицинской картой или страховым приложением, чтобы просмотреть всю информацию, содержащуюся в рецепте. - Неверный рецепт + Рецепты, импортированные из печатной копии, не могут отображать личную или медицинскую информацию по соображениям безопасности. \n\n Войдите в это приложение с помощью карты здоровья или страховки, чтобы просмотреть всю информацию, содержащуюся в рецепте. + Рецепт неправильный Этот рецепт был выписан неправильно. - Отсканированный рецепт - плата за экстренную помощь - Дозировка в соответствии с письменными инструкциями + Плата за аварийное обслуживание + Дозировка согласно письменной инструкции. Телефон - сайт + Веб-сайт Электронная почта Сортировка по расстоянию невозможна. OK Введите текущий PIN-код Введен неверный PIN-код - Текущий PIN-код вашей карты здоровья - Карточка заблокирована + Текущий PIN-код вашей медицинской карты + карта заблокирована Разблокируйте карту в меню «Настройки > «Разблокировать карту». В целях безопасности введите текущий PIN-код. - Забыли PIN-код + Забыт PIN-код Неправильный рецепт Препарат Похоже, что-то пошло не так при создании вашего рецепта. Сообщить об ошибке? Сообщить Вход не выполнен Зарегистрировано с - Медицинская карточка + страховой полис Биометрия Вход не выполнен - Нам интересно ваше мнение. Пожалуйста, найдите пять минут, чтобы ответить на вопросы нашего опроса. Заранее спасибо. - предупреждение - Аптека добавлена в избранное - Убрал аптеку из избранного + Нам интересно ваше мнение. Пожалуйста, уделите пять минут, чтобы заполнить наш опрос. Заранее большое спасибо. + Предупреждение + Аптека добавлена ​​в избранное + Аптека удалена из избранного Мои аптеки Надежность пароля очень высокая Операция записи не удалась - PIN-код не может быть сохранен + PIN-код не удалось сохранить. Сообщить Назначить PIN-код - Нарушено правило доступа + Правило доступа нарушено У вас нет разрешения на доступ к каталогу карт. - Назначьте свой собственный пин - Карта защищена PIN-кодом от вашей медицинской страховой компании (транспортный PIN-код), пожалуйста, присвойте свой собственный PIN-код. + Назначьте свой собственный PIN-код + Карта защищена PIN-кодом вашей медицинской страховой компании (транспортный PIN-код). Введите свой собственный PIN-код. Пароль не найден На вашей карте не хранится пароль. Вы вышли из системы - Войдите еще раз, чтобы обновить свои рецепты. - номер активного ингредиента - мощь и единство - Активирован %s минут назад + Войдите снова, чтобы обновить свои рецепты. + Номер действующего вещества + сила и единство + Погашен %s минут назад Погашено %s - Погашен только что - Погашен в %s часов + Погашен только сейчас + Погашено в %s часов заказы Этот рецепт был выкуплен для вас в рамках лечения. - плата за экстренную помощь - Этот рецепт не может быть выписан ночью в аптеке без дополнительной оплаты сбора за неотложную помощь. + Плата за аварийное обслуживание + Этот рецепт невозможно получить в аптеке в ночное время без дополнительной оплаты сбора за неотложную помощь. Поищи здесь Настройки - Поделитесь местоположением в настройках. + Поделиться местоположением в настройках. Близко ко мне - Удерживайте, чтобы отредактировать имя. + Нажмите и удерживайте, чтобы изменить имя. Введите новое имя профиля. - Вы должны войти в систему, чтобы получать цифровые рецепты из вашей практики. - Получать рецепты в цифровом виде? - Перетащите экран вниз, чтобы обновить. + Чтобы получать рецепты в цифровом формате из вашей практики, вам необходимо войти в систему. + Получать рецепты в цифровом формате? + Потяните экран вниз, чтобы обновить. Нет рецептов - Добавляйте рецепты с помощью кнопки + в правом верхнем углу. - Авторизоваться - архив рецептов + Добавляйте рецепты, используя кнопку + в правом верхнем углу. + регистр + Архив рецептов Может быть позже - Авторизоваться - Изменить изображение профиля - архив рецептов + регистр + Редактировать изображение профиля + Архив рецептов Ввести фамилию Сохранить Мой заказ Получатель: в Рецепты Аптека - послать - Изменить + Отправлять + Изменять Забрать в аптеке Доставка курьером Доставка по почте - %s рецептов - Выкупить невозможно - Не удалось активировать один или несколько рецептов. + %s Рецепты + Невозможно выкупить + Не удалось погасить один или несколько рецептов. Рецепт не выбран - Чтобы выкупить рецепты, необходимо выбрать хотя бы один рецепт. + Чтобы активировать рецепты, необходимо выбрать хотя бы один рецепт. Добавить контактную информацию - Изменить - Без рецепта - В настоящее время у вас нет погашаемых рецептов - поднимать - курьер + Изменять + Нет рецепта + В настоящее время у вас нет рецептов, подлежащих погашению. + коллекция + курьером Отправка - выбирать рецепты + Выбирайте рецепты Нажмите здесь, чтобы отсканировать рецепты Длительное нажатие для редактирования имен Добавьте больше профилей, например, для ваших детей или родителей - Нажмите на дисплей, чтобы пропустить отображаемую всплывающую подсказку. + Нажмите на дисплей, чтобы пропустить появившуюся всплывающую подсказку. Как выкупить? Как бы вы хотели получить лекарство? - Активировать напрямую - Выкупить лекарство на месте + Погасить напрямую + Получите лекарство на месте Заказать - Забронируйте или закажите доставку + Зарезервируйте или закажите доставку Готово - коллективный код - отдельные коды + Код коллекции + Индивидуальные коды - У вас есть %s рецепт . + У вас есть %s рецепт. У вас есть %s рецептов. @@ -757,58 +756,58 @@ Далее Узнать больше Указание - Это приложение использует программное обеспечение от Google для распознавания кодов. + Это приложение использует программное обеспечение Google для распознавания кодов. Узнать больше - О сканере кода рецепта + Информация о сканере кодов рецептов Какие данные содержит код рецепта? - Код рецепта содержит только идентификатор рецепта. Это позволяет найти рецепт в службе рецептов в цифровой сети здравоохранения. Код рецепта не содержит никаких данных о вас или вашем лекарстве. - То есть никто ничего не может сделать с одним только кодом рецепта? - Правильный. Данные рецепта должны быть загружены из службы рецептов. Для этого требуется безопасный вход. - Кто может зарегистрироваться в службе рецептов? - Регистрация в службе рецептов в сети цифрового здравоохранения возможна для застрахованных лиц, аптек, врачебных кабинетов и больниц. - Почему приложение электронных рецептов использует функции Google? - Google предлагает функции, которые можно легко встроить в приложения и которые Google постоянно разрабатывает и обновляет. Это гарантирует, что функции работают на многих различных конечных устройствах и могут работать безопасно. Приложение использует функцию для улучшения функций камеры и сканирования для устройств Android (Google ML Kit). - Как работает улучшение сканирования Google ML Kit? - Google ML Kit помогает оптимизировать изображение, снятое камерой, чтобы коды предписаний можно было считывать даже в условиях плохого освещения или на старых моделях камер. - Будут ли данные о рецепте или моем лекарстве передаваться в Google? - Нет. Считанный код рецепта сохраняется непосредственно в приложении и не передается в Google. Данные рецепта не хранятся в коде, только в цифровой сети здравоохранения. Оттуда они отправляются в приложение. Google не имеет доступа к сети цифрового здравоохранения. + Код рецепта содержит только идентификатор рецепта. Это означает, что рецепт можно найти в службе рецептов в цифровой сети здравоохранения. Код рецепта не содержит никакой информации о вас или вашем лекарстве. + Значит никто ничего не может сделать только с кодом рецепта? + Правильный. Данные рецепта необходимо загрузить из службы рецептов. Для этого необходим безопасный вход. + Кто может зарегистрироваться для получения рецептурной услуги? + Регистрация рецептурной услуги в сети цифрового здравоохранения возможна для застрахованных лиц, аптек, практик и больниц. + Почему приложение электронного рецепта использует функции Google? + Google предлагает функции, которые можно легко интегрировать в приложения и которые Google постоянно разрабатывает и обновляет. Это гарантирует, что функции будут работать на многих различных устройствах и их можно будет безопасно использовать. Приложение использует функцию улучшения функций камеры и сканирования для устройств Android (Google ML Kit). + Как работает улучшение сканирования с помощью Google ML Kit? + Google ML Kit помогает оптимизировать изображение, снятое камерой, чтобы коды рецептов можно было читать даже в условиях плохого освещения или на старых моделях камер. + Будут ли данные о рецепте или моем лекарстве переданы в Google? + Нет. Считанный код рецепта сохраняется прямо в приложении. Он не будет передан в Google. Данные рецепта хранятся не в коде, а только в цифровой сети здравоохранения. Оттуда они передаются в приложение. У Google нет доступа к сети цифрового здравоохранения. Какие данные обрабатывает Google при использовании ML Kit? - Google имеет доступ только к технической информации об используемом конечном устройстве и общем использовании дополнительной функции (например, частоте ошибок, настройках камеры) только для статистической регистрации и, таким образом, улучшения дополнительной функции. При доступе Google временно записывает IP-адрес вашего конечного устройства. Информация о вас и содержании рецепта не будет записана Google. + Google получает доступ только к технической информации об используемом устройстве и общем использовании дополнительной функции (например, частоте ошибок, настройках камеры) только для того, чтобы записать это статистически и тем самым улучшить дополнительную функцию. При доступе Google временно записывает IP-адрес вашего устройства. Информация о вас и содержание рецепта не фиксируются Google. Является ли использование Google ML Kit добровольным? - да Однако ML Kit встроен в сканер кодов рецептов в Android-версии приложения электронных рецептов.Если вы используете сканер кодов рецептов на устройстве Android, функция ML Kit также всегда используется. Однако можно обойтись и без использования сканера кодов предписаний. Ваши рецепты также могут быть загружены в приложение, если вы зарегистрируетесь в сети цифрового здравоохранения с помощью электронной карты здоровья или через приложение медицинского страхования. + Да. Однако ML Kit встроен в сканер кода рецептов в версии приложения для электронных рецептов для Android. Если вы используете сканер кода рецепта на устройстве Android, всегда используется функция ML Kit. Однако вы можете избежать использования сканера кода рецепта. Ваши рецепты также можно загрузить в приложение, если вы войдете в цифровую сеть здравоохранения с помощью электронной карты здоровья или через приложение медицинского страхования. Могу ли я увидеть, кто просматривал мои рецепты? - Да. Весь доступ к вашим данным полностью регистрируется в цифровой сети здравоохранения. В приложении электронного рецепта вы можете увидеть, кто получил доступ к вашим данным. - С кем я могу связаться, если у меня есть вопросы о приложении или электронном рецепте? - Вы можете найти подробную информацию в заявлении о защите данных. - Предписанное количество упаковок + Да. Весь доступ к вашим данным полностью регистрируется в цифровой сети здравоохранения. В приложении электронного рецепта вы можете увидеть, кто имел доступ к вашим данным. + Куда я могу обратиться, если у меня возникнут вопросы по поводу приложения или электронного рецепта? + Подробную информацию можно найти в заявлении о защите данных. + Количество прописанных упаковок Нет рецептов Для этого вам нужны погашаемые рецепты. Выбрать страховую организацию - Ищите страховку + Поиск страховки Отмена Что вы хотели бы заказать? Для этого приложения вам нужна карта и соответствующий PIN-код. Как бы вы хотели связаться со своей страховой компанией? - Ваша страховая компания предлагает следующие варианты контактов - Ваша страховая компания предлагает следующие варианты контактов + Ваша страховая компания предлагает следующие варианты связи + Ваша страховая компания предлагает следующий вариант связи Закрыть - PIN-код введен неправильно. - Номер доступа введен неправильно + PIN-код введен неверно. + Номер доступа введен неверно PUK введен неверно. - квитанции о расходах - Показать квитанции о расходах - квитанции о расходах + расходные квитанции + Просмотр квитанций о расходах + расходные квитанции Для получения квитанций о расходах необходимо подключение к серверу. Установить соединение - Нет квитанций о расходах + Никаких квитанций о расходах Деактивировать Отмена - отключить функцию - Это приведет к удалению всех квитанций с этого устройства и с сервера. - Получать квитанции о расходах - Квитанции о расходах также сохраняются на сервере рецептов. - Получать - Всего: %s %s + Деактивировать функцию + Это приведет к удалению всех квитанций о расходах с этого устройства и сервера. + Получайте квитанции о расходах + Ваши квитанции о расходах также сохраняются на сервере рецептов. + Полученный + Итого: %s %s Выберите Расколоть Удалить @@ -816,7 +815,7 @@ Представлять на рассмотрение %s € Итоговая цена - Совет: отправляйте квитанции о расходах через страховое приложение. + Совет: отправляйте квитанции о расходах через приложение страхования. Легко отправляйте квитанции о расходах через приложение вашей страховой компании. На следующем шаге выберите это приложение и нажмите «Поделиться». Упражняться Аптека @@ -827,36 +826,62 @@ КВНР: %s Дата рождения: %s OK - Как вы отправляете квитанции? - Передача непосредственно в приложение вашей страховой компании / бюро помощи. Для этого выберите приложение на следующей странице. + Как подать подтверждающие документы? + Перейдите непосредственно в приложение вашего страхового офиса. Для этого выберите приложение на следующей странице. или - Сохраните файл, а затем импортируйте его на портал страхования/помощи. + Сохраните файл, а затем импортируйте его на портал страхования/пособий. Статья: %s - Номер: %s + Количество: %s НДС: %s %% Цена брутто в евро: %s Дополнительная плата Плата за аварийное обслуживание - Комиссия BTM - Плата за рецепт Т - закупочные расходы + Комиссия БТМ + Плата за Т-рецепт + Затраты на закупки Курьерская доставка - Всего в евро: %s + Итого в евро: %s взимать - Реально удалить? - Файл будет удален с вашего устройства и с сервера. + Действительно удалить? + Файл будет удален с вашего устройства и сервера. Удалить Опубликовано Почтовый индекс Населенный пункт - Пожалуйста, введите свой почтовый индекс, чтобы связаться с нами. - Пожалуйста, укажите место жительства при обращении к нам. - Будет искуплен для вас + Пожалуйста, укажите свой почтовый индекс, чтобы связаться с нами. + Для связи с нами, пожалуйста, укажите свое место жительства. + Будет выкуплен за вас Был искуплен за тебя Вы должны войти в систему, чтобы использовать эту услугу. страховое приложение страховой полис Требуется соответствующий PIN-код + Можно использовать только завтра как самостоятельный плательщик + Осталось всего %s дней, чтобы использовать средства самостоятельной оплаты. + \nПо-прежнему можно погасить в качестве самостоятельного плательщика в течение %s дн.\n + Действительно только в течение %s дней + \nДействует еще %s дн.\n + Действует только завтра + Взимается дополнительная плата. + Берет страховку + Рецепт(ы) успешно перенесены. + Рецепт не может быть обработан. Пожалуйста, попробуйте еще раз. Возможно, вам придется выбрать другую аптеку. + Рецепт не может быть обработан. Аптека сообщает о неизвестной ошибке. При необходимости обратитесь в другую аптеку. + Рецепт был отклонен аптекой. Рецепт может быть недействительным, либо ваш адрес доставки или контактная информация могут быть недействительными. + Не удалось активировать, проверьте подключение к Интернету. + Рецепт успешно передан. Однако аптека сообщает об ошибке обработки. Пожалуйста, обратитесь в аптеку. + Рецепт был отклонен аптекой. Рецепт уже погашен. + Рецепт был отклонен аптекой. Рецепт удален. + Рецепт не удалось передать. Пожалуйста, проверьте подключение к Интернету и повторите попытку. + Не удалось перенести один или несколько рецептов. + Ошибка отправки + Отправлено успешно! + Ошибка в аптеке + Ошибка в аптеке + Контактная аптека + Рецепт уже выкуплен + Рецепт удален. + Без интернета Для получения логов доступа необходимо подключение к серверу. В течение этого периода вы по-прежнему можете получить рецепт в аптеке, но вам придется оплатить всю покупную стоимость лекарства самостоятельно. Кроме того, вы можете попросить свою практику переоформить рецепт. Готово @@ -865,4 +890,13 @@ В приложении Отсканируйте этот код в своей аптеке. Запрос на исправление платежа + Препарат + Пожалуйста, введите хотя бы 1 символ. + Или. Попробуйте приложение в демо-режиме + Демонстрационный режим + Демонстрационный режим + Использовать демонстрационный режим + Демонстрационный режим активирован + Конец здесь + Активировать демонстрационный режим diff --git a/android/src/main/res/values-ru/strings_kbv_codes.xml b/app/features/src/main/res/values-ru/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values-ru/strings_kbv_codes.xml rename to app/features/src/main/res/values-ru/strings_kbv_codes.xml diff --git a/android/src/main/res/values-tr/strings.xml b/app/features/src/main/res/values-tr/strings.xml similarity index 71% rename from android/src/main/res/values-tr/strings.xml rename to app/features/src/main/res/values-tr/strings.xml index a6122432..fc526ea9 100644 --- a/android/src/main/res/values-tr/strings.xml +++ b/app/features/src/main/res/values-tr/strings.xml @@ -16,32 +16,32 @@ Bu geçerli bir reçete kodu değil Bu reçete kodu zaten taranmış - %s reçete algılandı - %s reçete algılandı + %s tarifi tanındı + %s tarif tanındı İptal et Kamera ışığı - Reçete kodlarının taranması iptal edilsin mi? - Taramayı iptal et - Devam et - İşte başlıyoruz - İhtiyacınız olan şey: + Tarama iptal edilsin mi? + TAMAM + İptal etme + Hadi gidelim + Şunlara ihtiyacınız var: Kart erişim numarasını girin PIN girin Tekrar dene Sunucu bağlantısı başarısız oldu. - Kartınız bloke olmadan %s denemeniz kaldı. - Kartınız bloke olmadan %s denemeniz kaldı. + Kartınız bloke edilmeden önce %s bir deneme hakkınız daha var. + Kartınız bloke edilmeden önce %s deneme hakkınız daha var. - Erişim numarasını sağlık kartınızın sağ üst köşesinde bulacaksınız. + Erişim numarasını sağlık kartınızın sağ üst kısmında bulabilirsiniz. İptal et Kartı ara... Sağlık kartını cihazınızın arkasına doğru tutun. Hala aranıyor … Kartı cihazın arkasında yavaşça hareket ettirin. İpucu - Cihaz kılıfları, NFC üzerinden bağlantıyı zorlaştırabilir. + Cihaz kasaları NFC aracılığıyla bağlanmayı zorlaştırabilir. Kart algılandı Sağlık kartını hareket ettirmemeye çalışın. Sağlık kartı bulundu. Lütfen hareket etmeyin. @@ -61,29 +61,29 @@ İletişim Not Cinsiyet eşitliğine uygun bir dil kullanmaya çalışıyoruz. Herhangi bir hata fark ederseniz, sizden e-posta ile haber almaktan memnuniyet duyarız. - Almanya\'nın modern dijital tıp platformu + Almanya\'nın dijital tıp için modern platformu E-posta yaz Web sitesini aç Hoş geldiniz Oturum açmayı başlat - Kilidini aç - Oturum aç + Blokeyi kaldır + Giriş yapmak İptal et Güvenlik Yasal Künye Veri koruma - Kullanım koşulları + Kullanım şartları Ayrıntılar Kullanıldı olarak işaretle Kullanılmadı olarak işaretle İlaç türü - standart beden + Paket boyutu Sigortalı kişi Ad Adres Doğum tarihi - Sağlık sigortası / ödeyenler + Sağlık sigortası/ödeyen Durum Sigorta numarası Reçete yazan kişi @@ -95,7 +95,7 @@ Adres Kuruluş numarası Telefon numarası - E-posta + E-posta adresi İş kazası Kaza günü Kaza şirketi veya işveren numarası @@ -105,7 +105,7 @@ Açılış saatleri Web sitesi Sacede bugün ve sadece kendiniz ödeyerek kullanabilirsiniz - Oturum aç + Giriş yapmak NFC\'yi etkinleştir Sağlık kartınız ile oturum açmak için lütfen cihazınızın NFC fonksiyonunu etkinleştirin. Etkinleştir @@ -121,10 +121,10 @@ Ayarlar Ekran görüntülerini gizle Uygulamaları değiştirdiğinizde önizleme görüntüsünün görüntülenmesini engeller - E-Rezept\'in kullanıcı davranışınızı anonim olarak analiz etmesine izin veriyor musunuz? + E-Reçetenin kullanım davranışınızı anonim olarak analiz etmesine izin veriyor musunuz? Teknik bilgiler Reçete verilerinizin güvenliği - Lütfen bu cihazı paylaşabileceğiniz ve biyometrik özellikleri bu cihazda saklanabilecek veya cihaz PIN\'ini, kaydırma hareketini veya şifreyi bilen kişilerin de reçetelerinize erişebileceğini unutmayın. + Lütfen bu cihazı paylaşabileceğiniz ve biyometrik özellikleri bu cihazda saklanabilecek kişilerin de reçetelerinize erişebildiğinden emin olunuz. Gönderim başarısız oldu E-posta programı kurulmamış Sonuç yok @@ -137,15 +137,15 @@ Bu uygulamayı daha iyi hale getirmek için yardımcı olmak istiyorum Bu, telefonunuzun donanım ve yazılım bilgilerini, e-reçete uygulamasının ayarlarını ve kullanım kapsamını içerir, ancak asla kişiliğiniz veya sağlığınızla ilgili verileri içermez. Veriler, veri işleyenler tarafından sadece gematik GmbH\'ye sunulur ve en geç 180 gün sonra silinir. Analizi istediğiniz zaman uygulama menüsünden devre dışı bırakabilirsiniz. - Bu veriler, hangi işlevlerin sıklıkla kullanıldığını anlamamızı ve bunları geliştirmemizi sağlar. Ayrıca, daha eski teknolojinin ne kadar süre desteklenmesi gerektiğini ve örneğin (çok fazla) kullanıcıyı etkilemeden daha yeni bir işletim sistemi sürümünü ne zaman zorunlu hale getirebileceğimizi de değerlendirebiliriz. + Bu veriler hangi fonksiyonların sıklıkla kullanıldığını anlamamızı ve geliştirmemizi sağlar. Ayrıca eski teknolojinin ne kadar süreyle desteklenmesi gerektiğini ve örneğin (çok fazla) kullanıcıyı etkilemeden daha yeni bir işletim sistemi sürümünü ne zaman zorunlu hale getirebileceğimizi de tahmin edebiliriz. Uygulamayı iyileştir Anonim analiz devre dışı kalıyor %s Desteğiniz için teşekkürler! - Oturum aç + Giriş yapmak Reçeteyi indirmek için lütfen kimliğinizi doğrulayın. Eczaneler için not: Eczanelerin iletişim bilgilerini ve bilgilerini Deutscher Apothekenverband e.V.\'ın mein-apothekenportal.de adresinden alıyoruz. Bir hata mı buldunuz veya verileri düzeltmek mi istiyorsunuz? Daha fazla bilgi - Ezcaneler + eczaneler Bu maalesef olmadı \uD83D\uDE15 Lütfen tekrar deneyin. Şifreyi girin @@ -153,9 +153,9 @@ Kullanım yardımı Yakınlaştırma Parmaklarınızı bir araya getirmek veya ayırmak uygulamayı büyütmenizi sağlar (yakınlaştırmak için sıkıştırın). - Şifre + Parola Verilerinizi kendiniz seçtiğiniz şifre ile koruyun. - Şifre + Parola Kaydet Şifreyi göster Şifreyi tekrarla @@ -173,13 +173,13 @@ Anladım Tekrarlanan şifre eşleşiyor Hata 20 10 76631 - Sağlık kartınızın sertifikası geçerli değil. Kartınızın süresi dolmuş olabilir mi? Lütfen sağlık sigortanız ile iletişime geçin. + Sağlık kartı belgeniz geçersiz. Belki kartınızın süresi dolmuştur? Lütfen sağlık sigortası şirketinizle iletişime geçin. Başarısız oturum açma denemesi - %s başarısız oturum açma denemesi tespit edildi. - %s başarısız oturum açma denemesi tespit edildi. + Başarısız oturum açma denemelerinin %s tespit edildi. + %s başarısız oturum açma girişimi algılandı. - En iyi cihaz emniyetini seçme + En iyi cihaz emniyetini seçin Bu, bir parmak izi, bir silme deseni veya benzeri olabilir Tokenler Access Token @@ -188,7 +188,7 @@ Herhangi bir SSO Token mevcut değil Ara belleğe kopyalandı Token\'i ara belleğe kopyalamak için tıklayın - Sadece bugün geçerli + Yalnızca bugün geçerli İzin ver Sunucuya bağlantı yok Lütfen birkaç dakika sonra tekrar deneyin. @@ -204,7 +204,7 @@ Sunucuyla iletişim kurulamadı: Lütfen internet bağlantısını ve saat/tarih ayarlarını kontrol edin. Uyarı Cihazınızın güvenliği azaltılmış olabilir - Buna, örneğin manipüle edilmiş cihazlar veya etkinleştirilmiş bir geliştirici modu neden olabilir. Güvenlik nedeniyle, uygulamanın jailbreak\'li cihazlarda kullanılmasını önermiyoruz. + Bu durum, örneğin cihazların manipüle edilmesinden veya geliştirici modunun açılmasından kaynaklanabilir. Güvenlik nedeniyle uygulamayı jailbreakli cihazlarda kullanmamanızı öneririz. Yüksek riski kabul ediyor ve yine de devam etmek istiyorum. Kök erişimi bulunan cihazlar neden potansiyel bir güvenlik riski taşıyor? Daha fazla bilgi @@ -251,23 +251,23 @@ Kamera fonksiyonunu kullanırsam/reçeteleri kamera ile tararsam neler oluyor? Herhangi bir yeni reçete mevcut değil - %s yeni̇ reçete - %s yeni reçete + %s yeni tarifi + %s yeni tarifler Kullanılabilir kurtuluşta Kullanıldı Bilinmiyor Erişim protokollerini göster - Burada reçetelerinize kimlerin eriştiğini görebilirsiniz - Burada reçete hizmetine olan erişim anahtarı söz konusudur + Tariflerinize kim ve ne zaman erişti? + Reçete hizmetine erişim anahtarı Erişim protokolleri Herhangi bir erişim protokolü yok Halihazırda erişim protokolleri bulunmuyor. Reçete şu an düzenlenmekte ve silinemez - Kabul et + Kabul Bu maalesef başarılı olmadı - Sağlık kartıyla bağlantının bazı sorunların olduğunun farkındayız. Bu nedenle gelecekte, halihazırda kimliği doğrulanmış bir sağlık sigortası uygulaması aracılığıyla kayıt da mümkün olacaktır.\n\nAyrıca reçetelerin kayıt olmadan dijital olarak kullanılabilmesi için çalışıyoruz.\n\nBu süreçte bizimle paylaşmak istediğiniz bir şey mi fark ettiniz? Bu süreçte bizimle paylaşmak istediğiniz bir şey fark ettiniz mi? Lütfen bize yazın, son derece eleştirisel geri bildirimlerde bulunmanızdan da memnuniyet duyarız. + Sağlık kartıyla bağlantının bazı tuzakları olduğunun bilincindeyiz. Gelecekte, kimlik doğrulaması yapılmış bir sağlık sigortası uygulaması aracılığıyla kayıt da mümkün olacaktır. \n\n Reçetelerin kayıt olmadan dijital ortamda kullanılabilmesini sağlamak için de çalışıyoruz. \n\n Bu süreçte bizimle paylaşmak istediğiniz bir şey fark ettiniz mi? Lütfen bize yazın, çok kritik geri bildirimler almaktan da mutluluk duyarız. Bağlantı ip uçları Bağlantıyı güçlendirin Gerekirse koruyucu kılıfı çıkarın. @@ -296,8 +296,8 @@ Sonraki siparişler için kaydedin Reçeteleri cihaza kaydedin - %s reçete ile devam - %s reçete ile devam + %s tarifiyle devam et + %s tarifle devam et Sağlık kartının bağlanması başarısız oldu Güncel profil halihazırda bir başka sağlık kartı (sağlık sigorta numarası %s) ile bağlantılı. @@ -331,18 +331,18 @@ Engellendiniz mi? Lütfen bu cihazdaki biyometrik erişim verilerinizi tekrar kontrol edin. Şifreyi mi unuttunuz? Lütfen uygulamayı silin ve ardından yeniden yükleyin. Bunun neden böyle olduğunu şuradan öğrenebilirsiniz: %s. Yardım alanı - paket boyutu ve birimi + Paket boyutu ve birimi Etken madde Etken madde miktarı Parti adı Son kullanma tarihi: Kategori Aşı - Kabul et + Kabul Geri al Not Bu uygulamayı daha iyi hale getirmemize yardımcı olun - Kendi parolanızı seçin + Şifreyi girin Parola en az sekiz basamaklı olmalıdır Parola yeterince güçlü değil Parola yeterince güçlü @@ -361,7 +361,7 @@ Listenize reçeteler ekleyin. Bunun için sağ üst köşedeki tarama düğmesine tıklayın. Kağıt çıktıyı tara Reçeteleri otomatik olarak almak için oturum açmalısınız. - Oturum aç + Giriş yapmak Kullanılan reçete yok Burada kullanılmış reçeteleriniz gösterilir. Reçeteleriniz reçete sunucusundan 100 gün sonra veri koruma nedenlerinden dolayı silinir. Kullanılan reçete yok @@ -370,7 +370,7 @@ Bağlı cihazlar %s tarihinden beri kayıtlı (bu cihaz) %s tarihinden beri kayıtlı - Güvenlik nedeniyle reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için, her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. + Güvenlik nedeniyle, reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. PIN PIN\'inizi (sağlık kartı) girin. İleri @@ -416,9 +416,9 @@ Sağlık kartınız Sağlık kartınızın PUK\'u İleri - Sağlık kartı + Sigorta kartı PIN veya kart sipariş edin - Oturum aç + Giriş yapmak Nasıl oturum açmak istiyorsunuz? NFC özellikli sağlık kartı Sağlık kartının PIN\'i @@ -429,8 +429,8 @@ "Erişim numaranızı sağlık kartınızın sağ üst köşesinde bulacaksınız." Kartımın erişim numarası yok - Kartınız bloke edilmeden önce %s denemeniz var. - Kartınız bloke edilmeden önce %s denemeniz var. + Kartınız bloke edilmeden önce %s bir deneme hakkınız daha var. + Kartınız bloke edilmeden önce %s deneme hakkınız daha var. Sağlık kartını telefonun arkasına yerleştirin. Aşağıdaki proses 30 saniye sürebilir. @@ -452,7 +452,7 @@ Artık geçerli değil Uygulama ile oturum aç Sigortayı seç - Aradığınızı bulamadınız mı? Bu liste devamlı geliştiriliyor. Sağlık kartı ile oturum açılması halihazırda her sağlık sigortası tarafından desteklenmektedir. + Aradığınızı bulamadınız mı? Bu liste sürekli genişletilmektedir. Sağlık kartıyla kayıt zaten her sağlık sigortası şirketi tarafından desteklenmektedir. E-Rezept uygulamasından geri bildirimi Geri bildiriminizi bekliyoruz. Lütfen aşağıdaki alanı kullanın ve mümkün olduğunca detaylı yazın: PUK @@ -464,29 +464,29 @@ Kaydet Kaydetme Not - Güvenlik nedeniyle reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için, her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. + Güvenlik nedeniyle, reçete sunucusuna bağlantı 12 saat sonra sonlandırılır. Yeniden bağlanmak için her bağlantı işlemi için bir sağlık kartına ve PIN\'e ihtiyacınız vardır. Biyometrik güvenlik önlemi ayarla Erişim verilerini kaydetmek mümkün değil. Cihazınızda önceden biyometrik bir güvenlik önlemi (ör. parmak izi) ayarlayın. İptal et Ayarlar Not - Kabul et + Kabul Reçete verilerinizin güvenliği \"Bu uygulama cihazınızın kullanıma sunduğu en güvenli biyometrik sensörünü kullanıyor. Bu şekilde erişim verileriniz cihazınızın hafızasında güvenli bir alanda kaydedilir. \" Erişim verilerinizin biyometrik olarak kaydedilmesi, bu uygulamaya bundan sonra sağlık kartı olmadan ve PIN girmeden giriş yapılmasına, reçetelerin görüntülenmesine, açılmasına, kullanılmasına veya silinmesine olanak sağlar. - Bu cihazı başka kişilerle paylaşıyorsanız, biyometrik özellikleri bu cihazda saklanan kişilerin veya cihaz PIN\'ini, kaydırma hareketini veya şifreyi bilen kişilerin de reçetelerinize erişebildiğini lütfen dikkate alın. + Lütfen bu cihazı paylaşabileceğiniz ve biyometrik özellikleri bu cihazda saklanabilecek kişilerin de reçetelerinize erişebildiğinden emin olunuz. Bu maalesef olmadı - Sağlık sigortası ile kimlik doğrulama başarılı olmadı. + Sağlık sigortası uygulamasıyla kimlik doğrulama işlemi başarılı olmadı. %s tarihinde süresi doldu - Tarif zaten sunucudan silinmiş + Tarif zaten sunucudan silindi Lütfen girişinizi düzeltin veya değişiklikleri atın Düzelt - sigortalı veri + Sigortalı kişi verileri Ad - sigorta + Sigorta Sigorta numarası Kart erişim numarası - Oturum aç + Giriş yapmak Oturumu kapat Kaydet Değiştirmek @@ -495,303 +495,302 @@ sunucu yanıt vermiyor Lütfen daha sonra tekrar deneyiniz. Tekrar deneyin - sigorta ara + Sigorta ara Tarif sunucusuna şimdi bağlanılsın mı? Başarıyla giriş yaptı bağlantı koptu Tarif sunucusuna şimdi bağlanılsın mı? - jeton yok - Reçete hizmetine giriş yaptığınızda bir jeton alacaksınız.\n - emirler + Jeton yok + Reçete servisine giriş yaptığınızda bir jeton alacaksınız.\n + Emirler İstediğiniz PIN\'i seçin Kartın blokesini kaldırın PIN\'i seçin PIN\'i tekrarla Girişler birbirinden farklıdır. - sipariş yok - Henüz siparişiniz yok. + Sipariş yok + Henüz herhangi bir siparişiniz yok. Şu anda - %s saatinde + saat %s s\'de Alışveriş sepeti hazır - Tarif alışveriş sepetinize eklendi. Siparişi tamamlamak için lütfen eczanenin web sitesine gidin. + Tarif sepetinize eklendi. Siparişi tamamlamak için lütfen eczanenin web sitesine gidin. Alışveriş sepetini aç - Bu teslim alma kodunu eczanede gösterin. + Bu koleksiyon kodunu eczanede gösterin. Teslim alma kodu alındı Mesaj gösterilemiyor Lütfen eczanenizle iletişime geçin ( %s ). - Sepet bağlantısını göster + Alışveriş sepeti bağlantısını göster Teslim alma kodunu göster - mesajı göster - %s saat %s yönünde + Mesajı göster + %s saat %s s\'de Tarif %s adresine gönderildi. Siparişe genel bakış Yeni Kurs - Bir sipariş - Arayan için ücretsiz. Servis saatleri: Pazartesi - Cuma 08:00 - 20:00 Ulusal tatiller hariç - eczane + Emir + Arayan için ücretsizdir. Hizmet saatleri: Ulusal tatiller hariç Pazartesi - Cuma 08:00 - 20:00 + Eczane İstediğiniz PIN\'i seçin - İstenen PIN kaydedildi + İstenilen PIN kaydedildi Şu anda açık ve yakınımda Tarafından filtre … Aramaya başla - doğrudan atama + Doğrudan atama eczaneler - telefon numarası (isteğe bağlı) - Ada veya adrese göre arama + Telefon numarası (isteğe bağlı) + Ada veya adrese göre arayın Geçerli eczane bilgisi yok - Bu eczane hakkında güncel bilgi bulunamadı. Bu eczane için giriş silinecek. + Bu eczane hakkında güncel bilgi bulunamadı. Bu eczanenin girişi silinecek. Tamam Eczane rehberi mevcut değil - Şu anda bu eczane hakkında güncel bilgi alınamıyor. Lütfen internet bağlantınızı kontrol edin. + Şu anda bu eczaneye ait güncel bir bilgiye ulaşılamamaktadır. Lütfen internet bağlantınızı kontrol edin. İptal et Tekrar deneyin Ortamı Kaydet - Oturum açılamaz - Biyometrik oturum açma bilgileriniz değişmiş gibi görünüyor. Lütfen sağlık kartınızla tekrar kayıt olun. + Giriş mümkün değil + Biyometrik giriş özelliklerinizin değiştiği anlaşılıyor. Lütfen sağlık kartınızla tekrar giriş yapınız. İptal et - Oturum aç - profil 1 + Giriş yapmak + Profil 1 Bana yakın Daha sonra kullanılabilir - %s kullanılabilir - ürün iyileştirmeleri - Anonim Analiz - Bu uygulamayı daha iyi hale getirmemize yardımcı olun. Tüm kullanıcı verileri anonim olarak toplanır ve yalnızca kullanıcı deneyimini geliştirmek için kullanılır. - cihaz güvenliği + %s tarihinden itibaren kullanılabilir + Ürün iyileştirmeleri + Anonim analiz + Bu uygulamayı daha iyi hale getirmemize yardımcı olun. Tüm kullanım verileri anonim olarak toplanır ve yalnızca kullanıcı deneyimini geliştirmek için kullanılır. + Cihaz güvenliği kişisel ayarlar Kullanım yardımı - ürün iyileştirmeleri + Ürün iyileştirmeleri Tarif eklendi Tarif zaten mevcut İçe aktarılırken bir hata oluştu Sil Taranmış reçete Muadil mümkün - PIN\'i unuttum + Unutulan PIN - %s reçete - %s reçeteler + %s Tarif + %s Tarifler Gizlilik politikasını ve kullanım koşullarını okudum ve kabul ediyorum. Veri koruma politikası Kullanım Şartları - İsteriz: + Biz istiyoruz: Kullanılabilirliği geliştirin. Hataları ve çökmeleri tespit edin. Tüm veriler elbette anonim olarak toplanır. Bu kararı sistem ayarlarında her zaman değiştirebilirsiniz. Devam et - Kabul et + Kabul Bu uygulama, cihazınız tarafından sağlanan en güvenli yöntemi kullanır. Kaydet Seçmek - uyuşturucu - ticari unvan + ilaç + Ticari unvan Evet - hayır + HAYIR dozaj Veriliş tarihi Bu reçete, tedavinin bir parçası olarak sizin için kullanılacaktır. - Bilgi yok + Belirtilmemiş Ek ödeme - uyuşturucu - Teslimat notları + ilaç + Gönderim talimatları BVG\'ye göre uygun - alternatif hazırlık - formül adı + Alternatif hazırlık + Tarif adı Ambalajlama - işçiliği talimatı - tanım + Üretim talimatları + Tanım tarafından verilen üzerinde yayınlanan: Etken madde - reçete + Reçeteli Almak - Doğrudan atama nedir? - Doğrudan sevk durumunda, bir muayenehaneden veya hastaneden alınan reçete doğrudan eczanede kullanılır. Sigortalılar herhangi bir işlem yapmak zorunda değildir ve itfa sürecine müdahale edemezler. \n\n Tedavinizi sizin için daha şeffaf hale getirmek için e-reçete uygulamasında doğrudan yönlendirmeler listelenir. + Doğrudan görev nedir? + Doğrudan yönlendirmede, bir muayenehaneden veya hastaneden alınan reçete doğrudan eczanede doldurulur. Sigortalıların herhangi bir işlem yapmasına gerek yoktur ve geri ödeme sürecine müdahale edemezler. \n\n Tedavinizi sizin için daha şeffaf hale getirmek için e-reçete uygulamasında doğrudan yönlendirmeler listelenir. Acil servis ücreti Bazen acele etmek gerekebilir. Bazı reçeteler, örneğin geceleri veya resmi tatil günlerinde, acil servis ücreti ödenmeden doldurulabilir. Katkı payına tabi ilaçlar - Ortak ödemeden muaf - Yasal sağlık sigortası olanlar, reçeteli ilaçlar için on Euro\'ya kadar katkı payı ödemek zorundadır. \n\n Katkı payı miktarı, ilacınızın fiyatına bağlıdır. Maliyeti 5 €\'dan az olan ilaçlar için kendiniz ödeme yapmanız gerekir.\n Daha pahalı ilaçlar için fiyatın yüzde onunu, ancak en az 5 € ve maksimum 10 € ödemeniz gerekir. \n\n 18 yaşın altındaki çocuklar ve gençler genellikle katkı payından muaftır. \n\n Yıllık ilaç masraflarınız mali limitinizi aşarsa, katkı payından muaf olabilirsiniz. Bu konuda sağlık sigortanız ile konuşun. - Bu ilacın katkı payından muafsınız. Sağlık sigortanız ilacın maliyetini karşılayacaktır. + Ek ödemeden muaf + Yasal sağlık sigortası olanların reçeteli ilaçlar için on avroya kadar ek ödeme yapması gerekiyor. \n\n Ek ödemenin miktarı ilacınızın fiyatına bağlıdır. Maliyeti 5 €\'dan az olan ilaçları kendiniz ödemek zorundasınız.\n Daha pahalı ilaçlar için fiyatın yüzde onunu ödemeniz gerekir, ancak bu tutar en az 5 Euro, en fazla 10 Euro\'dur. \n\n 18 yaşın altındaki çocuklar ve gençler genel olarak ek ödemeden muaftır. \n\n Yıllık ilaç masraflarınız mali limitinizi aşıyorsa katkı payı ödemesinden muaf olabilirsiniz. Bu konuyu sağlık sigortası şirketinizle görüşün. + Bu ilaç için katkı payı ödemekten muafsınız. İlaç masraflarını sağlık sigortanız karşılayacaktır. Bu reçete ne kadar süreyle geçerlidir? Bu süre zarfında reçetenizi herhangi bir eczaneden maksimum 10 € ek ödemeyle alabilirsiniz. Muadil mümkün - Sağlık sigortanızın yasal zorunlulukları nedeniyle, aynı etken maddeye sahip bir alternatif sunulabilir. \n\n İlaçlar farklı görünebilir ve farklı adlandırılabilir, farklı fiyatlara ve üreticilere sahip olabilir, ancak yine de aynı etken maddeyi içerir. Aktif bileşenin kendisi ve dozajı, ilaçların vücuttaki etkisi için özellikle önemlidir. Eczanedeki hastalar, ilaçların karşılaştırılabilir olması koşuluyla, genellikle doktorun reçetede yazdığından farklı bir ilaç alırlar. Değişimin terapötik ve ekonomik nedenleri olabilir. + Sağlık sigortanızın yasal gereklilikleri nedeniyle size aynı etken maddeli bir alternatif sunulabilir. \n\n İlaçlar farklı görünebilir ve farklı adlandırılabilir, farklı fiyatlara ve üreticilere sahip olabilir, ancak yine de aynı etken maddeyi içerebilir. İlaçların vücuttaki etkisi açısından etken maddenin kendisi ve dozajı çok önemlidir. İlaçların karşılaştırılabilir olması koşuluyla, hastalar genellikle eczanede doktor tarafından reçete edilenden farklı bir ilaç alırlar. Değişimin tedavi edici ve ekonomik nedenleri olabilir. Taranmış reçete - Güvenlik nedeniyle, kağıt çıktıdan alınan reçeteler herhangi bir kişisel veya tıbbi veri içermemelidir. \n\n Reçetede yer alan tüm bilgileri görüntülemek için bu uygulamada sağlık kartı veya sigorta uygulamasıyla oturum açın. - Reçete yanlış - Bu reçete yanlış yazılmıştır. - Taranmış reçete - acil servis ücreti + Basılı kopyadan içe aktarılan reçeteler, güvenlik nedeniyle kişisel veya tıbbi bilgileri görüntüleyemez. \n\n Reçetede yer alan tüm bilgileri görüntülemek için bu uygulamaya sağlık kartı veya sigorta uygulamasıyla giriş yapın. + Tarif yanlış + Bu reçete yanlış düzenlenmiş. + Acil servis ücreti Yazılı talimatlara göre dozaj telefon - alan + İnternet sitesi E-posta Mesafeye göre sıralama mümkün değil. Tamam - Geçerli PIN\'i girin + Mevcut PIN\'i girin Yanlış PIN girildi - Sağlık kartınızın mevcut PIN\'i + Sağlık kartınızın güncel PIN kodu Kart bloke edildi - Ayarlar > Kartın engellemesini kaldır\'da kartınızın engellemesini kaldırın. + Ayarlar > Kartın Kilidini Aç bölümünden kartınızın kilidini açın. Güvenlik nedeniyle lütfen mevcut PIN\'inizi girin. - PIN\'i unuttum + Unutulan PIN Yanlış tarif - uyuşturucu - Tarifinizi oluştururken bir şeyler ters gitmiş gibi görünüyor. Hata bildir? - Bildiri + ilaç + Tarifinizi oluştururken bir şeyler ters gitmiş gibi görünüyor. Bir hata bildirilsin mi? + Rapor Oturum açılmamış - ile kayıtlı - Sağlık kartı + Kayıtlı + Sigorta kartı Biyometri Oturum açılmamış - Fikrinizle ilgileniyoruz. Lütfen anketimizi yanıtlamak için beş dakikanızı ayırın. Şimdiden teşekkür ederim. - uyarı notu + Fikrinizle ilgileniyoruz. Lütfen anketimizi tamamlamak için beş dakikanızı ayırın. Şimdiden çok teşekkür ederim. + Uyarı notu Eczane favorilere eklendi - Eczane Favorilerden Kaldırıldı - eczanelerim + Eczane favorilerden kaldırıldı + Eczanelerim Şifre gücü çok iyi - Yazma işlemi başarısız + Yazma işlemi başarılı değil PIN kaydedilemedi - Bildiri + Rapor PIN ata Erişim kuralı ihlal edildi Harita dizinine erişim izniniz yok. Kendi pininizi atayın - Kart, sağlık sigortanızın verdiği bir PIN (ulaşım PIN) ile güvence altına alınmıştır, lütfen kendi PIN kodunuzu atayın. + Kartınız sağlık sigortanızın PIN\'i (nakliye PIN\'i) ile güvence altına alınmıştır. Lütfen kendi PIN\'inizi girin. Şifre bulunamadı - Kartınızda kayıtlı şifre yok. + Kartınızda kayıtlı herhangi bir şifre bulunmamaktadır. Çıkış yaptınız - Reçetelerinizi güncellemek için tekrar oturum açın. - aktif madde numarası + Tariflerinizi güncellemek için tekrar giriş yapın. + Aktif madde numarası güç ve birlik %s dakika önce kullanıldı %s tarihinde kullanıldı Az önce kullanıldı - Saat %s konumunda kullanıldı - emirler + Saat %s de kullanıldı + Emirler Bu reçete, bir tedavinin parçası olarak sizin için kullanıldı. - acil servis ücreti - Bu reçete, acil servis ücreti ek ödemesi yapılmadan gece eczanede doldurulamaz. + Acil servis ücreti + Bu reçete, acil servis ücreti ek olarak ödenmeden gece eczanesinde doldurulamaz. Burada ara Ayarlar - Ayarlarda konumu paylaşın. + Ayarlar\'da konumu paylaşın. Bana yakın Adı düzenlemek için basılı tutun. Profil için yeni adı girin. - Muayenehanenizden dijital reçeteler almak için giriş yapmalısınız. + Muayenehanenizden dijital olarak reçete almak için oturum açmalısınız. Reçeteleri dijital olarak mı alıyorsunuz? - Yenilemek için ekranı aşağı sürükleyin. + Yenilemek için ekranı aşağı çekin. Reçete yok - Sağ üst köşedeki + düğmesini kullanarak reçete ekleyin. + Sağ üst köşedeki + düğmesini kullanarak tarifler ekleyin. Giriş yapmak - reçete arşivi + Tarif arşivi Belki sonra Giriş yapmak Profil resmini düzenle - reçete arşivi + Tarif arşivi İsim giriniz Kaydet Siparişim - alıcı: içinde + Alıcı: içinde Reçeteler - eczane + Eczane Göndermek - Değişmek - eczaneden al - kurye ile teslimat - Posta ile teslimat - %s reçeteler - Kullanılamaz + Değiştirmek + Eczaneden teslim alın + Kurye ile teslimat + Posta siparişi ile teslimat + %s Tarifler + Geri almak mümkün değil Bir veya daha fazla reçete kullanılamadı. - Reçete seçilmedi - Reçeteleri kullanmak için en az bir reçete seçilmelidir. - İletişim bilgilerini ekleyin - Değişmek - reçete yok - Şu anda kullanılabilecek reçeteniz yok - toplamak + Tarif seçilmedi + Tarifleri kullanmak için en az bir tarif seçilmelidir. + İletişim ayrıntılarını ekleyin + Değiştirmek + Tarif yok + Şu anda kullanılabilir reçeteniz yok + Toplamak kurye Kargo - reçeteleri seç - Reçeteleri taramak için buraya dokunun + Tarifleri seçin + Tarifleri taramak için buraya dokunun Adları düzenlemek için uzun basın Örneğin çocuklarınız veya ebeveynleriniz için daha fazla profil ekleyin Görüntülenen araç ipucunu atlamak için ekrana tıklayın. Nasıl geri alınır? İlaçlarınızı nasıl almak istersiniz? Doğrudan kullan - Sitede ilaç kullanın + İlaçları yerinde kullanın Sipariş ver Rezerve edin veya teslim ettirin - Bitti - kolektif kod - tek kodlar + Hazır + Koleksiyon kodu + Bireysel kodlar - Sizde %s reçetesi var. - Sizin %s reçeteniz var. + %s tarifiniz var. + %s tarifiniz var. seçim yap - Tüm reçeteler - Hangi reçeteler? + Tüm tarifler + Hangi tarifler? İleri İleri Daha fazla bilgi Not Bu uygulama, kodları tanımak için Google\'ın yazılımını kullanır. Daha fazla bilgi - Reçete kodu tarayıcı hakkında - Reçete kodu hangi verileri içerir? - Reçete kodu sadece reçetenin tanımlayıcısını içerir. Bu, reçetenin dijital sağlık ağındaki reçete hizmetinde bulunmasını sağlar. Reçete kodu, sizinle veya ilacınızla ilgili herhangi bir veri içermez. - Yani kimse sadece reçete koduyla bir şey yapamaz mı? - Doğru. Reçete verilerinin reçete servisinden indirilmesi gerekir. Bu, güvenli bir oturum açma gerektirir. + Tarif kodu tarayıcı hakkında bilgi + Tarif kodu hangi verileri içeriyor? + Tarif kodu yalnızca tarif için bir tanımlayıcı içerir. Bu, reçetenin dijital sağlık ağındaki reçete servisinde bulunabileceği anlamına gelir. Reçete kodu size veya ilacınıza ilişkin herhangi bir bilgi içermez. + Yani kimse sadece tarif koduyla bir şey yapamaz mı? + Doğru. Reçete verileri reçete servisinden indirilmelidir. Bunun için güvenli bir giriş gereklidir. Reçete hizmetine kimler kayıt olabilir? - Sigortalıların, eczanelerin, muayenehanelerin ve hastanelerin dijital sağlık ağında reçete hizmetine kaydolması mümkündür. + Sigortalıların, eczanelerin, muayenehanelerin ve hastanelerin dijital sağlık ağına reçete hizmetine kaydolması mümkündür. E-reçete uygulaması neden Google özelliklerini kullanıyor? - Google, uygulamalara kolayca yerleştirilebilen ve Google tarafından sürekli olarak geliştirilip güncellenen işlevler sunar. Bu, fonksiyonların birçok farklı uç cihazda çalışmasını ve güvenli bir şekilde çalıştırılabilmesini sağlar. Uygulama, Android cihazlar (Google ML Kit) için kamera ve tarama işlevini geliştirmeye yönelik bir özellik kullanır. - Google ML Kit tarama iyileştirmesi nasıl çalışır? - Google ML Kit, bir kamera tarafından çekilen görüntüyü optimize etmeye yardımcı olur, böylece reçete kodları zayıf aydınlatma koşullarında veya daha eski kamera modellerinde bile okunabilir. - Reçete veya ilacımla ilgili veriler Google\'a aktarılacak mı? - HAYIR. Okunan reçete kodu doğrudan uygulamaya kaydedilir ve Google\'a aktarılmaz. Reçete verileri kodda değil, yalnızca dijital sağlık ağında saklanır. Oradan uygulamaya gönderilirler.Google\'ın dijital sağlık ağına erişimi yoktur. - Google, ML Kit kullanırken hangi verileri işler? - Google, bunu istatistiksel olarak kaydetmek ve böylece ek işlevi iyileştirmek için yalnızca kullanılan son cihaz ve ek işlevin genel kullanımı (örn. hata oranı, kamera ayarları) hakkındaki teknik bilgilere erişebilir. Eriştiğinizde Google, uç cihazınızın IP adresini geçici olarak kaydeder. Hakkınızdaki bilgiler ve reçetenin içeriği Google tarafından kaydedilmeyecektir. - Google ML Kit\'in kullanımı gönüllü mü? - Evet Ancak ML Kit, e-reçete uygulamasının Android sürümündeki reçete kodu tarayıcısına yerleşiktir.Bir Android cihazda reçete kodu tarayıcı kullanıyorsanız, ML Kit işlevi de her zaman kullanılır. Ancak, reçete kodu tarayıcı kullanmadan yapabilirsiniz. Elektronik sağlık kartı veya sağlık sigortası uygulamanız üzerinden dijital sağlık ağına kaydolursanız reçeteleriniz de uygulamaya yüklenebilir. - Reçetelerime kimin baktığını görebilir miyim? - Evet. Verilerinize tüm erişimler tamamen dijital sağlık ağında kaydedilir. E-reçete uygulamasında verilerinize kimlerin ulaştığını görebilirsiniz. - Uygulama veya e-reçete ile ilgili sorularım olursa kiminle iletişime geçebilirim? + Google, uygulamalara kolaylıkla entegre edilebilecek ve Google\'ın sürekli olarak geliştirip güncellediği işlevler sunmaktadır. Bu, fonksiyonların birçok farklı cihazda çalışmasını ve güvenli bir şekilde çalıştırılabilmesini sağlar. Uygulama, Android cihazlar için kamera ve tarama işlevselliğini geliştirmeye yönelik bir özellik kullanır (Google ML Kit). + Tarama geliştirmesi Google ML Kit ile nasıl çalışır? + Google ML Kiti, bir kamera tarafından çekilen görüntünün optimize edilmesine yardımcı olur, böylece tarif kodları zayıf aydınlatma koşullarında veya daha eski kamera modellerinde bile okunabilir. + Reçetem veya ilaçlarımla ilgili veriler Google ile paylaşılacak mı? + HAYIR. Okunan tarif kodu doğrudan uygulamaya kaydedilir. Google ile paylaşılmayacaktır. Reçete verileri kodda değil, yalnızca dijital sağlık ağında saklanıyor. Oradan uygulamaya aktarılırlar. Google\'ın dijital sağlık ağına erişimi yoktur. + Google, ML Kit\'i kullanırken hangi verileri işler? + Google, yalnızca kullanılan cihaza ve ek işlevin genel kullanımına (örn. hata oranı, kamera ayarları) ilişkin teknik bilgilere, bunları istatistiksel olarak kaydetmek ve böylece ek işlevi iyileştirmek amacıyla erişim sağlar. Google, erişim sırasında cihazınızın IP adresini geçici olarak kaydeder. Hakkınızdaki bilgiler ve tarifin içeriği Google tarafından kaydedilmemektedir. + Google ML Kit\'in kullanımı isteğe bağlı mı? + Evet. Ancak ML Kit, e-reçete uygulamasının Android sürümündeki tarif kodu tarayıcıya yerleşiktir. Tarif kodu tarayıcısını bir Android cihazda kullanıyorsanız her zaman ML Kit işlevi kullanılır. Ancak tarif kodu tarayıcısını kullanmaktan kaçınabilirsiniz. Elektronik sağlık kartınız ile dijital sağlık ağına giriş yaptığınızda ya da sağlık sigortası uygulamanız üzerinden de reçeteleriniz uygulamaya yüklenebilmektedir. + Tariflerimi kimlerin görüntülediğini görebilir miyim? + Evet. Verilerinize tüm erişimler tamamen dijital sağlık ağına kaydedilir. E-reçete uygulamasında verilerinize kimlerin eriştiğini görebilirsiniz. + Uygulama veya e-reçeteyle ilgili sorularım olursa nereye başvurabilirim? Ayrıntılı bilgiyi veri koruma beyanında bulabilirsiniz. Reçete edilen paket sayısı Reçete yok - Bunun için paraya çevrilebilir reçetelere ihtiyacınız var. + Bunun için kullanılabilir reçetelere ihtiyacınız var. Sigortayı seç - sigorta ara + Sigorta ara İptal et Neye başvurmak istiyorsunuz? Bu uygulama için bir karta ve ilgili PIN\'e ihtiyacınız var. Sigorta şirketinizle nasıl iletişime geçmek istersiniz? - Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar - Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunar + Sigorta şirketiniz aşağıdaki iletişim seçeneklerini sunuyor + Sigorta şirketiniz aşağıdaki iletişim seçeneğini sunuyor Kapat PIN yanlış girildi. Erişim numarası yanlış girildi PUK yanlış girildi. - gider makbuzları - Gider makbuzlarını göster - gider makbuzları - Gider makbuzlarını almak için sunucuya bağlı olmanız gerekir. + Maliyet makbuzları + Maliyet makbuzlarını görüntüle + Maliyet makbuzları + Masraf makbuzlarını alabilmeniz için sunucuya bağlı olmanız gerekmektedir. Bağlan - Masraf makbuzu yok + Maliyet makbuzu yok Devre dışı bırakmak İptal et - işlevi devre dışı bırak - Bu, bu cihazdaki ve sunucudaki tüm makbuzları siler. - Gider makbuzlarını alma - Maliyet makbuzlarınız da reçete sunucusuna kaydedilir. - Almak + İşlevi devre dışı bırak + Bu işlem, bu cihazdaki ve sunucudaki tüm gider makbuzlarını silecektir. + Maliyet makbuzlarını alın + Maliyet makbuzlarınız da reçete sunucusunda saklanır. + Kabul edilmiş Toplam: %s %s Seçmek Bölmek @@ -800,47 +799,73 @@ Göndermek %s € toplam fiyat - İpucu: Gider makbuzlarını sigorta uygulaması aracılığıyla gönderin - Maliyet makbuzlarını sigorta şirketinizin uygulaması aracılığıyla kolayca gönderin. Bir sonraki adımda, bu uygulamayı seçin ve Paylaş\'a basın. + İpucu: Maliyet makbuzlarını sigorta uygulaması aracılığıyla gönderin + Sigorta şirketinizin uygulaması aracılığıyla maliyet makbuzlarını kolayca gönderin. Bir sonraki adımda bu uygulamayı seçin ve paylaş\'a basın. Pratik Eczane Tarih Daha fazla göster İlaç Kimliği - için verilmiş + için yayınlandı KVNR: %s Doğum tarihi: %s Tamam - Makbuzları nasıl gönderiyorsunuz? - Doğrudan sigorta şirketinizin/yardım ofisinizin uygulamasına aktarın. Bunu yapmak için sonraki sayfada uygulamayı seçin. + Destekleyici belgeleri nasıl gönderiyorsunuz? + Doğrudan sigorta/yardım ofisinizin uygulamasına aktarın. Bunu yapmak için sonraki sayfada uygulamayı seçin. veya Dosyayı kaydedin ve daha sonra sigorta/yardım portalına aktarın. Makale: %s Sayı: %s KDV: %s %% EUR cinsinden brüt fiyat: %s - Ek Ücretler + Ek ücretler Acil servis ücreti BTM ücreti - T reçete ücreti - satın alma maliyetleri + T-reçete ücreti + Tedarik maliyetleri Kurye hizmeti - EUR cinsinden toplam: %s - harç + Avro cinsinden toplam: %s + vergi Gerçekten silinsin mi? Dosya cihazınızdan ve sunucudan silinecektir. Sil Gönderildi Posta kodu Konum - Bizimle iletişime geçmek için lütfen posta kodunuzu girin. - Bizimle iletişime geçerken lütfen ikamet ettiğiniz yeri giriniz. - senin için kurtarılacak + Lütfen bizimle iletişime geçmek için posta kodunuzu girin. + Bizimle iletişime geçmek için lütfen ikamet ettiğiniz yeri belirtin. + Senin için kurtarılacak Senin için kurtarıldı - Bu hizmeti kullanmak için giriş yapmalısınız. + Bu hizmeti kullanabilmek için giriş yapmalısınız. sigorta uygulaması Sigorta kartı İlişkili PIN gerekli + Kendi kendine ödeme yapan kişi olarak yalnızca yarın kullanılabilir + Kendi kendine ödeme yapan kişi olarak kullanmak için yalnızca %s gün kaldı + \nHala %s gün süreyle kendi kendine ödeme yapan kişi olarak kullanılabilir\n + Yalnızca %s gün süreyle geçerlidir + \n%s gün kaldı\n + Sadece yarın geçerli + Ücretler uygulanır + Sigorta alır + Tarif(ler) başarıyla aktarıldı. + Tarif işlenemiyor. Lütfen tekrar deneyin. Farklı bir eczane seçmeniz gerekebilir. + Tarif işlenemiyor. Eczane bilinmeyen bir hata bildiriyor. Gerekirse başka bir eczaneyi deneyin. + Reçete eczane tarafından reddedildi. Reçete geçersiz olabilir veya teslimat adresiniz veya iletişim bilgileriniz geçersiz olabilir. + Kullanılamadı. Lütfen internet bağlantınızı kontrol edin. + Tarif başarıyla aktarıldı. Ancak eczane işlem hatası bildiriyor. Lütfen eczaneye başvurun. + Reçete eczane tarafından reddedildi. Reçete zaten kullanıldı. + Reçete eczane tarafından reddedildi. Tarif silindi. + Tarif aktarılamadı. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin. + Bir veya daha fazla tarif aktarılamadı. + Gönderim hatası + Başarıyla gönderildi! + Eczanede hata + Eczanede hata + Eczaneyle iletişime geçin + Reçete zaten kullanıldı + Tarif silindi + İnternet yok Erişim günlüklerini almak için sunucuya bağlı olmanız gerekir. Bu süre içinde reçeteyi yine eczanede doldurabilirsiniz, ancak ilacın satın alma bedelinin tamamını kendiniz ödemek zorunda kalacaksınız. Alternatif olarak muayenehanenizden reçetenin yeniden düzenlenmesini isteyebilirsiniz. Hazır @@ -849,4 +874,13 @@ Uygulamada Bu kodu eczanenizde tarattırın. Faturalandırma düzeltme talebi + ilaç + Lütfen en az 1 karakter girin. + Veya. Uygulamayı demo modunda deneyin + Demo modu + Demo modu + Demo modunu kullan + Demo modu etkinleştirildi + Burada bitir + Demo modunu etkinleştir diff --git a/android/src/main/res/values-tr/strings_kbv_codes.xml b/app/features/src/main/res/values-tr/strings_kbv_codes.xml similarity index 100% rename from android/src/main/res/values-tr/strings_kbv_codes.xml rename to app/features/src/main/res/values-tr/strings_kbv_codes.xml diff --git a/android/src/main/res/values-uk/strings.xml b/app/features/src/main/res/values-uk/strings.xml similarity index 75% rename from android/src/main/res/values-uk/strings.xml rename to app/features/src/main/res/values-uk/strings.xml index 5f7c17b5..548df4b4 100644 --- a/android/src/main/res/values-uk/strings.xml +++ b/app/features/src/main/res/values-uk/strings.xml @@ -16,27 +16,27 @@ Це недійсний код рецепта Цей код рецепта уже відскановано - Розпізнано %s рецепт - Розпізнано %s рецепти - Розпізнано %s рецептів - + %s рецепт розпізнано + + + Виявлено %s рецептів Скасувати Світло камери - Скасувати сканування кодів рецептів? - Скасувати сканування - Продовжити - Почнімо + Скасувати сканування? + в порядку + Не скасовувати + Ходімо Вам потрібно: Введіть номер доступу до картки Ввести PIN-код Спробуйте ще раз Помилка з’єднання з сервером - У вас ще %s спроба, перш ніж буде заблоковано картку - У вас ще %s спроби, перш ніж буде заблоковано картку - У вас ще %s спроб, перш ніж буде заблоковано картку - + У вас є ще %s одна спроба, перш ніж вашу картку буде заблоковано. + + + У вас є ще %s спроб, перш ніж вашу картку буде заблоковано. Ви знайдете номер доступу у верхньому правому куті своєї картки здоров\'я. Скасувати @@ -49,7 +49,7 @@ Картку розпізнано Намагайтеся не рухати карту здоров’я. Картку здоров’я знайдено. Не рухайтеся. - Підключення скасовано + З\'єднання перервано Ще раз прикладіть картку здоров’я до задньої панелі пристрою. Версія: %s Хеш збірки: %s @@ -59,7 +59,7 @@ Вихідні дані Видавець gematik GmbH\nFriedrichstraße 136\n10117 Berlin - Керівний директор: д-р. мед. н Маркус Лейк Дікен (Markus Leyck Dieken)\n Реєстраційний суд: окружний суд Берлін-Шарлоттенбург\n Номер торгового реєстру: HRB 96351\nІН платника ПДВ: DE241843684 + Керівний директор: д-р. мед. н Маркус Лейк Дікен (Markus Leyck Dieken)\nРеєстраційний суд: окружний суд Берлін-Шарлоттенбург\nНомер торгового реєстру: HRB 96351\nІН платника ПДВ: DE241843684 Відповідальний за контент Доктор мед. н. Маркус Лейк Дікен Контакт @@ -71,7 +71,7 @@ Вітаємо! Розпочати реєстрацію Розблокувати - Вхід + зареєструватися Скасувати Безпека Правові питання @@ -82,7 +82,7 @@ Позначити як погашено Позначити як не погашено Лікарська форма - стандартний розмір + Розмір пакування Застрахована особа Прізвище Адреса @@ -99,7 +99,7 @@ Адреса Номер виробничого майданчика Номер телефону - Ел. пошта + Адреса ел. пошти Нещасний випадок на виробництві День нещасного випадку Номер місця нещасного випадку або номер роботодавця @@ -109,7 +109,7 @@ Графік роботи: Вебсайт Ще тільки сьогодні рецепт можна погасити, якщо ви оплачуєте самостійно. - Вхід + зареєструватися Активувати NFC Активуйте функцію NFC на своєму пристрої, щоб увійти за допомогою своєї картки здоров’я. Активувати @@ -125,10 +125,10 @@ Налаштування Придушити скріншоти Запобігає відображенню заставки під час переходу з одного застосунку до іншого - Чи дозволяєте ви застосунку E-Rezept анонімно аналізувати поведінку користувача? + Чи дозволяєте ви E-Prescription анонімно аналізувати вашу поведінку при використанні? Технічна інформація Безпека ваших даних рецепта - Майте на увазі, що особи, з якими ви, можливо, спільно користуєтеся цим пристроєм і чиї біометричні функції можуть зберігатися на цьому пристрої, або які мають PIN-код пристрою, графічний ключ або пароль, також матимуть доступ до ваших рецептів. + Майте на увазі, що особи, з якими ви, можливо, спільно користуєтеся цим пристроєм і чиї біометричні функції можуть зберігатися на цьому пристрої, також матимуть доступ до ваших рецептів. Помилка відправлення Не налаштований жодний поштовий клієнт Немає результатів @@ -141,23 +141,23 @@ Я хочу допомогти покращити цей застосунок Це включає інформацію про апаратне та програмне забезпечення на вашому телефоні, налаштування застосунку E-Rezept та обсяг використання, однак у жодному разі не дані про вас особисто чи ваше здоров’я. Ці дані надаються тільки gematik GmbH компанією, яка обробляє дані, та видаляються не пізніше ніж через 180 днів. Аналіз можна деактивувати в меню застосунку в будь-який час. - Ці дані дозволяють нам зрозуміти, які функції часто використовуються, і покращити їх. Крім того, ми можемо оцінити, як довго має підтримуватися старіша технологія і коли ми можемо, наприклад, зробити нову версію операційної системи обов’язковою, щоб це не зачепило (занадто багато) користувачів. + Ці дані дозволяють нам зрозуміти, які функції часто використовуються, і вдосконалити їх. Ми також можемо оцінити, як довго потрібно підтримувати старі технології та коли ми можемо, наприклад, зробити новішу версію операційної системи обов’язковою, не впливаючи на (надто багато) користувачів. Покращити застосунок Анонімний аналіз залишається деактивованим %s Дякуємо за Вашу підтримку! - Вхід + зареєструватися Щоб завантажити рецепти, ідентифікуйте себе. Примітка для аптек: ми отримуємо контактні дані та інформацію про аптеки від mein-apothekenportal.de Німецької аптечної асоціації Deutsche Apothekenverband e.V. Ви виявили помилку чи хотіли б виправити дані? Детальніше - Аптеки + аптеках На жаль, спроба невдала \uD83D\uDE15 - Повторіть спробуй + Будь ласка, спробуйте ще раз. Введіть пароль Далі Довідки з керування Масштабувати Дозволяє збільшувати масштабування застосунку зведенням або розведенням пальців (зведення для масштабування). - Пароль: + Пароль Захистіть свої дані паролем на свій вибір. Пароль Зберегти @@ -177,13 +177,13 @@ Зрозуміло Паролі не збігаються Помилка 20 10 76631 - Сертифікат вашої картка здоров\'я недійсний. Можливо, термін дії вашої картки закінчився? Зв’яжіться зі своєю медичною страховою компанією. + Ваша медична картка недійсна. Можливо у вас закінчився термін дії картки? Будь ласка, зв\'яжіться зі своєю медичною страховою компанією. Невдалі спроби входу - Виявлено %s вдалу спробу входу в систему. - Виявлено %s вдалі спроби входу в систему. - Виявлено %s вдалих спроб входу в систему. - + Виявлено %s невдалих спроб входу. + + + Виявлено %s невдалих спроб входу. Вибрати найкращий захист пристрою Це може бути відбиток пальця, графічний ключ або щось подібне. @@ -194,7 +194,7 @@ не доступний жоден токен SSO скопійовано в буфер обміну Натисніть, щоб скопіювати токен у буфер обміну - дійсний ще тільки сьогодні + Діє лише сьогодні Дозволити Підключення до сервера відсутнє Спробуйте ще раз через кілька хвилин. @@ -210,7 +210,7 @@ Не вдалося зв’язатися із сервером: перевірте підключення до Інтернету та налаштування часу/дати. Попередження Ваш пристрій може мати знижений рівень безпеки - Це може бути спричинено, наприклад, маніпуляціями з пристроями або активованим режимом розробника. З міркувань безпеки ми не рекомендуємо використовувати програму на зламаних пристроях. + Це може бути спричинено, наприклад, маніпуляціями з пристроями або коли ввімкнено режим розробника. Ми рекомендуємо не використовувати програму на зламаних пристроях з міркувань безпеки. Я беру до уваги підвищений ризик, та все ж хочу продовжувати. Чому пристрої з root-доступом становлять потенційну загрозу безпеці? Детальніше @@ -239,7 +239,7 @@ З\'єднано Дата останнього підключення: %s Видалити профіль? - Таким чином буде видалено всі дані профілю на цьому пристрої. Ваші рецепти в мережі охорони здоров’я залишаться неушкодженими. + Це призведе до видалення всіх даних із профілю на цьому пристрої. Ваші рецепти в мережі охорони здоров\'я будуть збережені. Видалити Скасувати Видалити профіль @@ -257,25 +257,25 @@ Що станеться, якщо я використаю функцію камери / зчитаю рецепти за допомогою камери? Нових рецептів немає - %s новий рецепт + Новий рецепт %s - %s нових рецепта + %s нових рецептів Можна погасити У спокуті Погашено Невідомо Відобразити протоколи доступу - Тут ви можете побачити, хто мав доступ до ваших рецептів - Мова про ключ доступу до сервісу рецептів + Хто і коли отримував доступ до ваших рецептів? + Ключ доступу до сервісу рецептів Протоколи доступу Протоколи доступу відсутні Протоколів доступу ще немає. Наразі рецепт обробляється, і його неможливо видалити. - Прийняти + прийняти Мабуть, спроба невдала. - Ми усвідомлюємо, що підключення до картки здоров’я має свої \"підводні камені\". Тому в майбутньому вхід також стане можливим через уже автентифікований застосунок лікарняної каси.\n\nКрім того, ми працюємо над тим, щоб рецепти можна було погашати в цифровій формі навіть без входу в систему.\n\nЧи помітили ви під час цього процесу щось, про що хотіли би повідомити нам? Напишіть нам, ми будемо раді навіть дуже критичним відгукам. + Ми знаємо, що підключення до медичної картки має свої підводні камені. У майбутньому реєстрація також повинна бути можливою через уже автентифікований додаток медичного страхування. \n\n Ми також працюємо над тим, щоб рецепти можна було викупити в цифровому вигляді без реєстрації. \n\n Чи помітили ви щось під час цього процесу, чим хотіли б поділитися з нами? Будь ласка, напишіть нам, ми також будемо раді отримати дуже критичні відгуки. Поради щодо підключення Покращте силу з\'єднання За можливості видаліть захисну оболонку @@ -304,10 +304,10 @@ Зберегти для пізніших замовлень Зберегти рецепти на пристрої - Далі з %s рецептом - Далі з %s рецептами - Далі з %s рецептами - + Продовжте з рецептом %s + + + Продовжити %s рецептів Помилка підключення картки здоров’я Поточний профіль уже підключений до іншої картки здоров\'я (номер медичного страхування: %s). @@ -341,18 +341,18 @@ Заблоковано? Перевірте свої біометричні облікові дані на цьому пристрої. Забули пароль? Видаліть застосунок, а потім повторно встановіть його. Чому це так, ви можете дізнатися в %s Довідка - розмір упаковки та одиниця + Розмір упаковки та одиниця Активна речовина Кількість активної речовини Позначення партії Використовувати до Категорія Вакцина - Прийняти + прийняти Скасувати Указівка Допоможіть нам зробити цю програму кращою - Вибрати власний пароль + Введіть пароль Пароль повинен містити не менше восьми символів Надійність пароля не достатня Надійність пароля достатня @@ -371,7 +371,7 @@ Додайте рецепти до свого списку, натиснувши кнопку сканування у верхньому правому куті. Зісканувати видрук Щоб автоматично отримувати рецепти, ви повинні увійти в систему. - Вхід + зареєструватися Непогашених рецептів немає Тут відображаються ваші погашені рецепти. З міркувань захисту персональних даних ваші рецепти будуть видалені з сервера рецептів через 100 днів. Непогашених рецептів немає @@ -384,7 +384,7 @@ PIN Ввести PIN-код (картки здоров’я) Далі - Увійти + зареєструватися Підключені пристрої Видалити пристрій? Скасувати @@ -407,28 +407,28 @@ Ми зібрали для вас поради щодо розв\'язання найпоширеніших проблем. Запустити поради щодо підключення Розблокувати - Картка заблокована + картку заблоковано PIN-код тричі введено неправильно. Таким чином, ваша картка заблокована з міркувань безпеки. - Розблокувати картку - Ввести PUK-код - Разом з PIN-кодом ви отримали від своєї медичної страхової компанії 8-значний PUK-код. + розблокувати картку + Введіть PUK + Разом із PIN-кодом ви отримали 8-значний PUK від вашої страхової компанії. Вибрати новий PIN-код - Ви можете самостійно вибрати свій новий персональний ідентифікаційний номер (PIN-код) (від 6 до 8 цифр). - Запам\'ятали PIN-код? + Ви можете самостійно вибрати свій новий персональний ідентифікаційний номер (ПІН) (від 6 до 8 цифр). + PIN-код запам\'ятали? Занотуйте собі свій PIN-код та зберігайте його в надійному місці. Скасувати Ok Розблокування не можливе За допомогою цього PUK-коду ви досягли максимальної кількості розблокувань картки або ще раз ввели його неправильно. Зв’яжіться зі своєю страховою компанією. - Один PUK-код можна використовувати для 10 розблокувань. - Картка розблокована + Ви можете використовувати один PUK для 10 розблокувань. + картку розблоковано Вам потрібно: Ваша картка здоров\'я PUK-код вашої картки здоров\'я Далі - Картка здоров\'я + страхова картка Замовити ПІН або картку - Вхід + зареєструватися Як ви хочете ввійти? Картка здоров’я з підтримкою NFC PIN-код картки здоров’я @@ -439,10 +439,10 @@ "Ваш номер доступу ви знайдете у верхньому правому куті своєї картки здоров\'я." Моя картка не має номера доступу - У вас ще %s спроба, перш ніж буде заблоковано картку - У вас ще %s спроби, перш ніж буде заблоковано картку - У вас ще %s спроб, перш ніж буде заблоковано картку - + У вас є ще %s одна спроба, перш ніж вашу картку буде заблоковано. + + + У вас є ще %s спроб, перш ніж вашу картку буде заблоковано. Прикласти картку здоров\'я до задньої панелі телефона Цей процес може тривати до 30 секунд. @@ -464,7 +464,7 @@ Більше не дійсний Увійдіть за допомогою застосунку Вибрати страховку - Не знайшли те, що шукали? Цей список постійно розширюється. Реєстрацію за допомогою картки здоров\'я вже підтримує кожна лікарняна каса. + Не знайшли те, що шукали? Цей список постійно розширюється. Реєстрація за медичною карткою вже підтримується кожною медичною страховою компанією. Відгук із застосунку E-Rezept Ми з нетерпінням чекаємо на ваші відгуки. Використовуйте місце нижче та формулюйте свої думки якомога точніше: PUK @@ -482,23 +482,23 @@ Скасувати Налаштування Указівка - Прийняти + прийняти Безпека ваших даних рецепта \"Цей застосунок використовує найбезпечніший біометричний датчик, наданий вашим пристроєм, щоб зберігати ваші облікові дані в безпечній області пам’яті пристрою.\" Біометрична безпека ваших даних доступу дозволяє вам відкривати цю програму в майбутньому, не вводячи свій PIN-код або картку здоров’я, а також переглядати, викликати, використовувати або видаляти рецепти. - Майте на увазі, що особи, з якими ви, можливо, спільно користуєтеся цим пристроєм і чиї біометричні функції можуть зберігатися на цьому пристрої, або які мають PIN-код пристрою, графічний ключ або пароль, також матимуть доступ до ваших рецептів. + Майте на увазі, що особи, з якими ви, можливо, спільно користуєтеся цим пристроєм і чиї біометричні функції можуть зберігатися на цьому пристрої, також матимуть доступ до ваших рецептів. На жаль, спроба невдала. - Аутентифікація за допомогою застосунку лікарняної каси була невдалою. + Автентифікація за допомогою програми медичного страхування не пройшла успішно. Термін дії минув %s Рецепт уже видалено з сервера - Виправте введені дані або скасуйте зміни + Будь ласка, виправте свій запис або скасуйте зміни Відкоректувати Дані застрахованої особи Прізвище Страхування Страховий номер Номер доступу до картки - Вхід + зареєструватися Вийти з системи Зберегти Зміна @@ -507,16 +507,16 @@ Сервер не відповідає Повторіть спробу пізніше. Спробуйте ще раз - Шукайте страховку - Зараз підключитися до сервера рецептів? + Пошук страховки + Підключитися до сервера рецептів? Успішно ввійшли - зв\'язок втрачено + зв\'язок втрачений Підключитися до сервера рецептів? Ніяких жетонів - Ви отримаєте жетон, коли зареєструєтесь у службі рецептів.\n + Ви отримаєте жетон під час входу в службу рецептів.\n замовлення Виберіть потрібний PIN-код - Розблокувати картку + розблокувати картку Виберіть PIN-код Повторіть PIN-код Записи відрізняються один від одного. @@ -527,11 +527,11 @@ Кошик для покупок готовий Рецепт додано у ваш кошик. Щоб оформити замовлення, перейдіть на сайт аптеки. Відкрити кошик - Покажіть цей код отримання в аптеці. + Покажіть цей код колекції в аптеці. Отримати код для самовивозу Неможливо відобразити повідомлення Будь ласка, зверніться до вашої аптеки ( %s ). - Показати посилання на кошик + Показати посилання на кошик для покупок Показати код для самовивозу Показати повідомлення %s о годині %s @@ -539,7 +539,7 @@ Огляд замовлення новий курс - замовлення + Замовити Безкоштовно для абонента. Графік роботи: Пн – Пт 8:00 – 20:00, крім національних свят Аптека Виберіть потрібний PIN-код @@ -547,42 +547,42 @@ Наразі відкрито і біля мене Фільтрувати за… почати пошук - пряме доручення + Пряме доручення аптеках - номер телефону (необов\'язково) - Пошук за прізвищем або адресою + Номер телефону (необов\'язково) + Пошук за назвою або адресою Немає дійсної інформації про аптеку Актуальної інформації про цю аптеку не знайдено. Запис про цю аптеку буде видалено. Ok Каталог аптек недоступний - Наразі актуальну інформацію про цю аптеку отримати неможливо. Перевірте підключення до Інтернету. + Наразі немає актуальної інформації про цю аптеку. Перевірте підключення до Інтернету. Скасувати Спробуйте ще раз Зберегти довкілля Вхід неможливий - Здається, ваші біометричні облікові дані для входу змінилися. Будь ласка, зареєструйтеся знову за допомогою вашої медичної картки. + Схоже, ваші біометричні характеристики входу змінилися. Будь ласка, увійдіть ще раз за допомогою своєї картки здоров\'я. Скасувати - Вхід - профіль 1 + зареєструватися + Профіль 1 Поруч зі мною Викупити пізніше Можна отримати від %s - вдосконалення продукту + Покращення продукту Анонімний аналіз - Допоможіть нам зробити цю програму кращою. Усі дані користувача збираються анонімно та використовуються лише для покращення взаємодії з користувачем. - безпека пристрою + Допоможіть нам зробити цю програму кращою. Усі дані про використання збираються анонімно та використовуються виключно для покращення взаємодії з користувачем. + Безпека пристрою персональні налаштування Довідки з керування - вдосконалення продукту + Покращення продукту Доданий рецепт Рецепт вже є Під час імпортування сталася помилка Видалити Відсканований рецепт Можливий замінник - Забув PIN-код + Забутий PIN-код - %s рецепт + %s Рецепт %s Рецепти @@ -596,76 +596,75 @@ Усі дані, звичайно, збираються анонімно. В налаштуваннях системи ви можете змінити це рішення у будь-який час. Продовжити - Прийняти + прийняти Ця програма використовує найбезпечніший метод, доступний вашим пристроєм. Зберегти Виберіть Медикамент Торгова назва Так - ні + Немає дозування дата випуску Цей рецепт буде використано для вас як частина лікування. Дані відсутні - додаткова оплата + Додаткова оплата Медикамент - Накладні на доставку + Інструкції щодо подання Відповідає вимогам BVG - альтернативна підготовка - назва формули + Альтернативна підготовка + Назва рецепта Упаковка - інструкція по виготовленню + Інструкція з виготовлення опис дається видано: Активна речовина - прописаний + Прописаний Отримати Що таке пряме доручення? - У разі прямого направлення, рецепт від практики або лікарні викуповується безпосередньо в аптеці. Застраховані особи не повинні вживати жодних дій і не можуть втручатися в процес викупу. \n\n Прямі напрямки перераховані в додатку для електронних рецептів, щоб зробити ваше лікування більш прозорим для вас. + При прямому направленні рецепт від практики або лікарні виписується безпосередньо в аптеці. Застраховані особи не повинні вживати жодних дій і не можуть втручатися в процес викупу. \n\n Прямі напрямки перераховані в додатку для електронних рецептів, щоб зробити ваше лікування більш прозорим для вас. Плата за аварійну службу Іноді потрібно поспішати. Деякі рецепти можна отримати без доплати за екстрену допомогу, наприклад, вночі або у святкові дні. - Препарати, що підлягають співоплаті - Звільнено від співоплати - Ті, хто має державне медичне страхування, повинні сплачувати доплату в розмірі до десяти євро за рецептурні ліки. \n\n Сума співоплати залежить від вартості ваших ліків. За ліки, які коштують менше 5 євро, ви повинні платити самі.\n За ліки, які коштують дорожче, потрібно заплатити десять відсотків від ціни, але не менше 5 євро, а максимум 10 євро. \n\n Діти та молодь віком до 18 років, як правило, звільнені від співоплати. \n\n Якщо ваші річні витрати на ліки перевищують ваш фінансовий ліміт, ви можете бути звільнені від співоплати. Поговоріть про це зі своїм медичним страхувальником. - Ви звільнені від спільної оплати цього препарату. Ваше медичне страхування покриває вартість ліків. + Ліки підлягають співоплаті + Звільнено від додаткової оплати + Ті, хто має державне медичне страхування, повинні сплачувати додаткову плату в розмірі до десяти євро за рецептурні ліки. \n\n Розмір доплати залежить від вартості Вашого препарату. За ліки, які коштують менше 5 євро, ви повинні платити самі.\n За ліки, які коштують дорожче, потрібно заплатити десять відсотків від ціни, але не менше 5 євро, а максимум 10 євро. \n\n Діти та молодь віком до 18 років, як правило, звільняються від додаткової плати. \n\n Якщо ваші річні витрати на ліки перевищують ваш ліміт фінансового тягаря, ви можете бути звільнені від співоплати. Поговоріть про це зі своєю медичною страховою компанією. + Ви звільнені від сплати спільної оплати за цей препарат. Ваша медична страхова компанія покриває вартість ліків. Скільки дійсний цей рецепт? Протягом цього періоду ви можете отримати рецепт у будь-якій аптеці з максимальною додатковою оплатою 10 євро. Можливий замінник - Відповідно до юридичних вимог вашої страхової компанії, вам можуть надати альтернативу з тим самим діючим інгредієнтом. \n\n Ліки можуть виглядати і називатися по-різному, мати різні ціни та виробників, але містити однакову діючу речовину. Сам активний інгредієнт і дозування особливо важливі для впливу ліків на організм. Пацієнти в аптеці часто отримують інший препарат, ніж той, який виписав лікар за рецептом – за умови, що препарати порівнювані. Для зміни можуть бути терапевтичні та економічні причини. + Відповідно до юридичних вимог вашої страхової компанії, вам можуть надати альтернативу з тим самим активним інгредієнтом. \n\n Ліки можуть виглядати і називатися по-різному, мати різні ціни та виробників, але містити однакову діючу речовину. Сам активний інгредієнт і дозування мають вирішальне значення для впливу ліків на організм. Пацієнти часто отримують в аптеці ліки, відмінні від тих, які призначив лікар, за умови, що ліки порівнюються. Для зміни можуть бути терапевтичні та економічні причини. Відсканований рецепт - З міркувань безпеки рецепти, імпортовані з паперової роздруківки, не повинні містити жодних особистих чи медичних даних. \n\n Увійдіть у цю програму за допомогою медичної картки або програми страхування, щоб переглянути всю інформацію, що міститься в рецепті. - Неправильний рецепт + Рецепти, імпортовані з паперової копії, не можуть відображати особисту або медичну інформацію з міркувань безпеки. \n\n Увійдіть у цю програму за допомогою медичної картки або програми страхування, щоб переглянути всю інформацію, що міститься в рецепті. + Рецепт невірний Цей рецепт був виписаний неправильно. - Відсканований рецепт - плата за екстрену службу + Плата за аварійну службу Дозування відповідно до письмових інструкцій Телефон - сайт + веб-сайт Ел. пошта Сортування за відстанню неможливе. Ok Введіть поточний PIN-код Введено неправильний PIN-код Поточний PIN-код вашої медичної картки - Картка заблокована + картку заблоковано Розблокуйте картку в Налаштуваннях > Розблокувати картку. З міркувань безпеки введіть поточний PIN-код. - Забув PIN-код + Забутий PIN-код Невірний рецепт Медикамент Здається, під час створення вашого рецепта щось пішло не так. Повідомити про помилку? Повідомити про порушення Не в системі Зареєстрований с - Картка здоров\'я + страхова картка Біометрія Не в системі - Нам цікава ваша думка. Приділіть п’ять хвилин, щоб відповісти на наше опитування. Спасибі заздалегідь. - попереджувальне повідомлення + Нам цікава ваша думка. Будь ласка, приділіть п’ять хвилин, щоб заповнити наше опитування. Заздалегідь дуже дякую. + Попередження Аптека додана в обране - Аптеку видалено з вибраного + Аптека видалена з вибраного Мої аптеки Надійність пароля дуже добра Операція запису не вдалася @@ -675,12 +674,12 @@ Правило доступу порушено Ви не маєте дозволу на доступ до каталогу карт. Призначте власний PIN-код - Картка захищена PIN-кодом вашої страхової компанії (транспортний PIN-код), будь ласка, призначте свій власний PIN-код. + Картка захищена PIN-кодом від вашої страхової компанії (транспортний PIN-код). Будь ласка, введіть свій власний PIN-код. Пароль не знайдено На вашій картці не збережено пароль. Ви вийшли з системи Увійдіть знову, щоб оновити свої рецепти. - номер активного інгредієнта + Номер діючої речовини потужність і єдність Використано %s хвилин тому Використано %s @@ -688,25 +687,25 @@ Використано о %s годині замовлення Цей рецепт був викуплений для вас як частина лікування. - плата за екстрену службу - Цей рецепт не можна отримати вночі в аптеці без додаткової оплати екстреної послуги. + Плата за аварійну службу + Цей рецепт не можна отримати в аптеці вночі без додаткової оплати екстреної послуги. Шукайте тут Налаштування Поділитися місцезнаходженням у налаштуваннях. Поруч зі мною - Утримуйте, щоб редагувати назву. + Натисніть і утримуйте, щоб змінити назву. Введіть нову назву для профілю. - Ви повинні увійти в систему, щоб отримати цифрові рецепти від вашої практики. + Щоб отримати рецепти в цифровому вигляді від вашої практики, ви повинні увійти в систему. Отримувати рецепти в цифровому вигляді? - Перетягніть екран вниз, щоб оновити. + Потягніть екран вниз, щоб оновити. Кожних рецептів Додайте рецепти за допомогою кнопки + у верхньому правому куті. - Увійти - архів рецептів + зареєструватися + Архів рецептів Можливо пізніше - Увійти + зареєструватися Редагувати зображення профілю - архів рецептів + Архів рецептів Введіть ім\'я Зберегти Моє замовлення @@ -714,27 +713,27 @@ Рецепти Аптека Надіслати - Змінювати + Зміна Забрати в аптеці Доставка кур\'єром Доставка поштою - %s рецептів - Викупити неможливо + %s Рецепти + Неможливо викупити Один або кілька рецептів не вдалося викупити. Рецепт не вибрано - Щоб викупити рецепти, потрібно вибрати принаймні один рецепт. - Додайте контактну інформацію - Змінювати - Без рецепта + Щоб отримати рецепти, потрібно вибрати принаймні один рецепт. + Додайте контактні дані + Зміна + Немає рецепту Наразі у вас немає рецептів, які можна викупити - пікап + колекція кур\'єр Розсилання - вибрати рецепти + Виберіть рецепти Торкніться тут, щоб сканувати рецепти Тривале натискання для редагування імен Додайте більше профілів, наприклад, для ваших дітей або батьків - Клацніть на дисплеї, щоб пропустити відображену підказку. + Клацніть на дисплеї, щоб пропустити підказку, яка з’явиться. Як викупити? Як би ви хотіли отримати ваші ліки? Викупити безпосередньо @@ -742,10 +741,10 @@ Замовити Забронюйте або доставте Готово - колективний код - одиничні коди + Код колекції + Індивідуальні коди - Ви маєте %s рецепт. + У вас є %s рецепт. У вас є %s рецептів. @@ -759,55 +758,55 @@ Указівка Ця програма використовує програмне забезпечення від Google для розпізнавання кодів. Детальніше - Про сканер кодів рецептів + Інформація про сканер кодів рецептів Які дані містить код рецепта? - Код рецепту містить лише ідентифікатор рецепту. Це дозволяє знайти рецепт у службі рецептів у цифровій мережі охорони здоров’я. Код рецепта не містить жодних даних про вас чи ваші ліки. - Отже, ніхто нічого не може зробити з кодом рецепта? + Код рецепта містить лише ідентифікатор рецепту. Це означає, що рецепт можна знайти в службі рецептів у цифровій мережі охорони здоров’я. Код рецепта не містить жодної інформації про вас або ваші ліки. + Тож ніхто нічого не може зробити лише з кодом рецепту? Правильно. Дані про рецепт необхідно завантажити зі служби рецептів. Для цього потрібен безпечний вхід. Хто може зареєструватися в рецептурній службі? - Реєстрація в службі рецептів у цифровій мережі охорони здоров’я можлива для застрахованих осіб, аптек, медичних практик і лікарень. + Реєстрація в службі рецептів у цифровій мережі охорони здоров’я можлива для застрахованих осіб, аптек, практик і лікарень. Чому додаток для електронних рецептів використовує функції Google? - Google пропонує функції, які можна легко вбудувати в програми та які Google постійно розробляє та оновлює. Це гарантує, що функції працюють на багатьох різних кінцевих пристроях і ними можна безпечно керувати. Додаток використовує функцію для покращення камери та функцій сканування для пристроїв Android (Google ML Kit). - Як працює покращення сканування Google ML Kit? - Google ML Kit допомагає оптимізувати зображення, отримане камерою, щоб коди рецептів можна було зчитувати навіть в умовах поганого освітлення або за допомогою старих моделей камер. - Чи будуть дані про рецепт або мої ліки передані в Google? - Немає. Прочитаний код рецепта зберігається безпосередньо в додатку. Він не буде передано в Google. Дані про рецепти не зберігаються в коді, лише в цифровій мережі охорони здоров’я. Звідти вони надсилаються в додаток Google не має доступу до цифрової мережі охорони здоров’я. + Google пропонує функції, які можна легко інтегрувати в програми та які Google постійно розробляє та оновлює. Це гарантує, що функції працюють на багатьох різних пристроях і ними можна безпечно користуватися. Додаток використовує функцію для покращення камери та функцій сканування для пристроїв Android (Google ML Kit). + Як покращення сканування працює з Google ML Kit? + Google ML Kit допомагає оптимізувати зображення, зняте камерою, щоб коди рецептів можна було прочитати навіть в умовах поганого освітлення або за допомогою старих моделей камер. + Чи будуть дані про рецепт або мої ліки надані Google? + Немає. Прочитаний код рецепта зберігається безпосередньо в додатку. Його не буде надано Google. Дані про рецепти зберігаються не в коді, а лише в цифровій мережі охорони здоров’я. Звідти вони передаються в додаток. Google не має доступу до цифрової мережі охорони здоров’я. Які дані обробляє Google під час використання ML Kit? - Google має доступ лише до технічної інформації про використовуваний кінцевий пристрій і загальне використання додаткової функції (наприклад, частота помилок, налаштування камери), щоб зафіксувати це статистично та таким чином покращити додаткову функцію. Під час доступу Google тимчасово записує IP-адресу вашого кінцевого пристрою. Інформація про вас і вміст рецепта не буде записана Google. + Google отримує доступ лише до технічної інформації про використовуваний пристрій і загальне використання додаткової функції (наприклад, частота помилок, налаштування камери), щоб зафіксувати це статистично та таким чином покращити додаткову функцію. Під час доступу Google тимчасово записує IP-адресу вашого пристрою. Інформація про вас і вміст рецепта не записуються Google. Чи є використання Google ML Kit добровільним? - так Однак ML Kit вбудовано в сканер кодів рецептів у версії програми для електронних рецептів для Android. Якщо ви використовуєте сканер кодів рецептів на пристрої Android, функція ML Kit також використовується завжди. Однак ви можете обійтися без використання сканера кодів рецептів. Ваші рецепти також можна завантажити в додаток, якщо ви зареєструєтесь у цифровій мережі охорони здоров’я за допомогою електронної медичної картки або через програму медичного страхування. + Так. Однак ML Kit вбудовано в сканер кодів рецептів у версії програми для електронних рецептів для Android. Якщо ви використовуєте сканер коду рецепта на пристрої Android, завжди використовується функція ML Kit. Однак ви можете не використовувати сканер коду рецепта. Ваші рецепти також можна завантажити в додаток, якщо ви ввійдете в цифрову мережу охорони здоров’я за допомогою електронної медичної картки або за допомогою програми медичного страхування. Чи можу я побачити, хто переглядав мої рецепти? Так. Весь доступ до ваших даних повністю реєструється в цифровій мережі охорони здоров’я. У додатку для електронних рецептів ви можете побачити, хто мав доступ до ваших даних. - До кого я можу звернутися, якщо у мене є запитання щодо програми чи електронного рецепта? - Ви можете знайти детальну інформацію в декларації про захист даних. + Куди я можу звернутися, якщо у мене є запитання щодо програми чи електронного рецепта? + Детальну інформацію можна знайти в декларації про захист даних. Приписана кількість упаковок Кожних рецептів Для цього вам потрібні викупні рецепти. Вибрати страховку - Шукайте страховку + Пошук страховки Скасувати Бажаєте подати заявку? Для цієї програми вам потрібна картка та відповідний PIN-код. Як би ви хотіли зв’язатися зі своєю страховою компанією? Ваша страхова компанія пропонує такі способи зв’язку - Ваша страхова компанія пропонує такі способи зв’язку + Ваша страхова компанія пропонує наступний спосіб зв’язку Закрити PIN-код введено неправильно. Номер доступу введено неправильно PUK введено неправильно. видаткові квитанції - Показати видаткові квитанції + Переглянути квитанції видаткові квитанції Для отримання видаткових квитанцій необхідно підключитися до сервера. Підключити - Відсутність видаткових квитанцій + Відсутність квитанцій Дезактивувати Скасувати - відключити функцію - Це видалить усі квитанції з цього пристрою та із сервера. - Отримувати видаткові квитанції - Ваші квитанції також зберігаються на сервері рецептів. - Отримати + Дезактивувати функцію + Це видалить усі квитанції про витрати з цього пристрою та сервера. + Отримувати квитанції про витрати + Ваші чеки також зберігаються на сервері рецептів. + Отримано Усього: %s %s Вибрати Спліт @@ -827,36 +826,62 @@ КВНР: %s Дата народження: %s Ok - Як ви подаєте квитанції? - Передайте безпосередньо в додаток вашої страхової компанії/служби допомоги. Для цього виберіть додаток на наступній сторінці. + Як подати підтверджуючі документи? + Перенесіть безпосередньо в додаток вашого страхового/пільгового офісу. Для цього виберіть додаток на наступній сторінці. або - Збережіть файл і пізніше імпортуйте його на портал страхування/допомоги. + Збережіть файл і пізніше імпортуйте його на портал страхування/пільг. Стаття: %s - Номер: %s + Кількість: %s ПДВ: %s %% Ціна брутто в євро: %s Додаткові збори Плата за аварійну службу Комісія BTM - T збір за рецептом - витрати на закупівлю + Плата за Т-рецептом + Заготівельні витрати Кур\'єрська служба Усього в євро: %s збір Справді видалити? - Файл буде видалено з вашого пристрою та з сервера. + Файл буде видалено з вашого пристрою та сервера. Видалити Опубліковано Пошт. індекс Нас. пункт - Будь ласка, введіть свій поштовий індекс, щоб зв\'язатися з нами. - Будь ласка, вкажіть своє місце проживання при зверненні до нас. + Будь ласка, вкажіть свій поштовий індекс, щоб зв’язатися з нами. + Для зв\'язку з нами вкажіть, будь ласка, місце проживання. Буде викуплено для вас Був викуплений для вас Ви повинні увійти в систему, щоб скористатися цією послугою. страховий додаток страхова картка Потрібен пов\'язаний PIN-код + Можна викупити лише завтра як самооплату + Залишилося лише %s днів, щоб скористатися послугою самостійного сплати + \nВсе ще можна отримати як самооплату протягом %s днів\n + Дійсний лише протягом %s днів + \nЗалишилося %s днів\n + Діє лише завтра + Стягується плата + Бере страховку + Рецепт(и) успішно передано. + Рецепт не підлягає обробці. Будь ласка спробуйте ще раз. Можливо, вам доведеться вибрати іншу аптеку. + Рецепт не підлягає обробці. Аптека повідомляє про невідому помилку. Якщо потрібно, спробуйте іншу аптеку. + Рецепт був відхилений аптекою. Рецепт може бути недійсним або ваша адреса доставки чи контактна інформація можуть бути недійсними. + Не вдалося активувати, перевірте підключення до Інтернету. + Рецепт успішно передано. Однак аптека повідомляє про помилку обробки. Будь ласка, зверніться в аптеку. + Рецепт був відхилений аптекою. Рецепт вже погашений. + Рецепт був відхилений аптекою. Рецепт видалено. + Не вдалося передати рецепт. Перевірте підключення до Інтернету та повторіть спробу. + Не вдалося передати один або кілька рецептів. + Помилка відправки + Успішно відправлено! + Помилка в аптеці + Помилка в аптеці + Зверніться в аптеку + Рецепт уже погашений + Рецепт видалено + Немає Інтернету Щоб отримати журнали доступу, ви повинні бути підключені до сервера. Ви все ще можете виписати рецепт в аптеці протягом цього терміну, але вам доведеться заплатити всю вартість ліків самостійно. Крім того, ви можете попросити свою практику повторно виписати рецепт. Готово @@ -865,4 +890,13 @@ У додатку Відскануйте цей код у своїй аптеці. Запит на виправлення рахунку + Медикамент + Введіть принаймні 1 символ. + Або. Спробуйте програму в демо-режимі + Демо-режим + Демо-режим + Використовуйте демонстраційний режим + Демо-режим активовано + Кінець тут + Активуйте демонстраційний режим diff --git a/app/features/src/main/res/values/colors.xml b/app/features/src/main/res/values/colors.xml new file mode 100644 index 00000000..94ebfd09 --- /dev/null +++ b/app/features/src/main/res/values/colors.xml @@ -0,0 +1,20 @@ + + + #FF000000 + #FFF5F5F5 + #FF616161 + #FF212121 + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #FFFFFFFF + #000000 + #000000 + #FF9E9E9E + #FF3182CE + #FF2B6CB0 + #FFFFEBEB + #FFF56565 + #FF742A2A + + \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/app/features/src/main/res/values/strings.xml similarity index 99% rename from android/src/main/res/values/strings.xml rename to app/features/src/main/res/values/strings.xml index d0b04e15..bef468be 100644 --- a/android/src/main/res/values/strings.xml +++ b/app/features/src/main/res/values/strings.xml @@ -551,7 +551,7 @@ Bestellübersicht Neu Verlauf - Bestellung + Die Bestellung Kostenfrei für den Anrufenden. Servicezeiten: Mo - Fr 08:00 - 20:00 Uhr außer an bundeseinheitlichen Feiertagen Apotheke Wunsch-PIN wählen @@ -652,7 +652,6 @@ Von einem Papierausdruck importierte Rezepte dürfen aus Sicherheitsgründen keine persönlichen oder medizinische Daten anzeigen.\n\nMelden Sie sich in dieser App mit Gesundheitskarte oder Versicherungs-App an, um alle im Rezept enthaltenen Informationen einzusehen. Rezept fehlerhaft Dieses Rezept wurde fehlerhaft ausgestellt. - Gescanntes Rezept Notdienstgebühr Dosierung gemäß schriftlicher Anweisung Telefon @@ -908,4 +907,13 @@ In der App Lassen Sie diesen Code in Ihrer Apotheke abscannen. Korrekturanfrage der Abrechnung + Medikament + Bitte geben Sie mindestens 1 Zeichen ein. + Oder. Die App im Demo-Modus ausprobieren + Demo-Modus + Demo-Modus + Demomodus nutzen + Demo-Modus aktiviert + Hier Beenden + Demo-Modus aktivieren diff --git a/app/features/src/main/res/values/strings_desktop.xml b/app/features/src/main/res/values/strings_desktop.xml new file mode 100644 index 00000000..deb46bfc --- /dev/null +++ b/app/features/src/main/res/values/strings_desktop.xml @@ -0,0 +1,61 @@ + + + Info + Nutzungsbedingungen + Datenschutz + + E-Rezept + Die App für Desktop + Mit Gesundheitskarte anmelden + + Rezepte + Apotheken + Einlösbar + Bereits eingelöst + Benachrichtigungen + Protokoll + + Aktualisieren + Vergrößern + Verkleinern + Hilfe + Abmelden + + Keine Angabe + + Nutzungsbedingungen & Datenschutz + Kartenlesegerät anschließen + Kartenzugangsnummer eingeben + PIN eingeben + Mit Gesundheitskarte anmelden + + Suche nach Kartenlesegerät + Kartenlesegerät gefunden + Treiber des Kartenlesegeräts möglicherweise nicht geladen. Bitte verbinden Sie das Kartenlesegerät vor dem Start der E-Rezept-Anwendung. + + Lesen der Gesundheitskarte fehlgeschlagen. + Legen Sie die Gesundheitskarte erneut auf das Lesegerät. + + + Nur noch heute als Selbstzahlender einlösbar + Noch %s Tag als Selbstzahlender einlösbar + Noch %s Tage als Selbstzahlender einlösbar + + + Nur noch heute gültig + Noch %s Tag gültig + Noch %s Tage gültig + + Nicht mehr gültig + Ausgestellt am %s + Rezeptcode anzeigen + + Abholung + Botendienst + Versand + + https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz + https://www.das-e-rezept-fuer-deutschland.de/app/datenschutz + https://www.das-e-rezept-fuer-deutschland.de/faq + https://www.das-e-rezept-fuer-deutschland.de/kontakt + \ No newline at end of file diff --git a/app/features/src/main/res/values/strings_kbv_codes.xml b/app/features/src/main/res/values/strings_kbv_codes.xml new file mode 100644 index 00000000..3c3ca58a --- /dev/null +++ b/app/features/src/main/res/values/strings_kbv_codes.xml @@ -0,0 +1,285 @@ + + + Mitglied + Familienangehörig + Rentner*in + Keine Angabe + Keine therapiegerechte Packungsgröße + Normgröße 1 + Normgröße 2 + Normgröße 3 + Nicht betroffen + Sonstiges + Ätherisches Öl + keine Darreichungsform + Digitale Gesundheitsanwendungen + Ampullen + Ampullenpaare + Augen- und Nasensalbe + Augen- und Ohrensalbe + Augen- und Ohrentropfen + Augentropfen + Augenbad + Augencreme + Augengel + Augensalbe + Bad + Balsam + Bandage + Beutel + Binden + Bonbons + Basisplatte + Brei + Brausetabletten + Creme + Durchstechflaschen + Dilution + Depot-Injektionssuspension + Dragées in Kalenderpackung + Dosieraerosol + Dragées + Magensaftresistente Dragées + Dosierschaum + Dosierspray + Einzeldosis-Pipette + Einreibung + Elektroden + Elixier + Emulsion + Essenz + Erwachsenen-Suppositorien + Extrakt + Filterbeutel + Franzbranntwein + Filmdragées + Fertigspritze + Fettsalbe + Flasche + Flüssigkeit zum Einnehmen + Flüssig + Magensaftresistente Filmtabletten + Folie + Beutel mit retardierten Filmtabletten + Flüssigseife + Filmtabletten + Granulat zur Entnahme aus Kapseln + Gel + Gas und Lösungsmittel zur Herstellung einer Injektions-/Infusionsdispersion + Globuli + Magensaftresistentes Granulat + Gelplatte + Garnulat + Granulat zur Herstellung einer Suspension zum Einnehmen + Gurgellösung + Handschuhe + Magensaftresistente Hartkapseln + Hartkapseln + Hartkapseln mit Pulver zur Inhalation + Hartkapseln mit veränderter Wirkstofffreisetzung + Infusionsampullen + Infusionsbeutel + Infusionsdispersion + Injektionslösung in einer Fertigspritze + Infusionsflaschen + Infusionslösungskonzentrat + Injektions-Flaschen + Infusionsset + Inhalations-Ampullen + Inhalations-Pulver + Injektions- oder Infusionslösung oder Lösung zum Einnehmen + Injektions- Infusions-Lösung + Injektionslösung zur intramuskulären Anwendung + Inhalations-Kapseln + Injektions-Lösung + Implantat + Infusionslösung + Inhalat + Injektions- Infusions-Flaschen + Inhalations-Lösung + Instant-Tee + Instillation + Injektions-Suspension + Intrauterinpessar + Kanülen + Kapseln + Katheter + Kaudragées + Kegel + Kerne + Kaugummi + Konzentrat zur Herstellung einer Infusionsdispersion + Konzentrat zur Herstellung einer Injektions- oder Infusionslösung + Kleinkinder-Suppositorien + Klistiere + Klistier-Tabletten + Hartkapseln mit Magensaft-resistent überzogenen Pellets + Magensaftresistente Kapseln + Kondome + Kompressen + Konzentrat + Kombipackung + Kristallsuspension + Kinder- und Säuglings-Suppositorien + Kinder-Suppositorien + Kautabletten + Lanzetten + Lösung zur Injektion, Infusion oder Inhalation + Liquidum + Lösung + Lotion + Lösung für einen Vernebler + Lösung zum Einnehmen + Lacktabletten + Lutschpastillen + Lutschtabletten + Milch + Mischung + Mixtur + Magensaftresistentes Retardgranulat + Magensaftresistente Pellets + Manteltabletten + Mundwasser + Nasengel + Nasenöl + Nasenspray + Wirkstoffhaltiger Nagellack + Nasendosierspray + Nasensalbe + Nasentropfen + Occusert + Öl + Ohrentropfen + Ovula + Packungsmasse + Pastillen + Pellets + Injektionslösung in einem Fertigpen + Perlen + Pflaster + Pflaster-transdermal + Pulver zur Herstellung einer Injektions- Infusions- oder Inhalationslösung + Pulver zur Herstellung einer Injektions- bzw. Infusionslösung oder Pulver und Lösungsmittel zur Herstellung einer Lösung zur intravesikalen Anwendung + Pulver für ein Konzentrat zur Herstellung einer Infusionslösung Pulver zur Herstellung einer Lösung zum Einnehmen + Pulver zur Herstellung einer Infusionslösung + Pulver zur Herstellung einer Injektions- oder Infusionslösung + Pulver zur Herstellung einer Injektionslösung + Pulver zur Herstellung eines Infusionslösungskonzentrats + Pulver zur Herstellung einer Infusionssuspension + Pulver zur Herstellung einer Injektions- bzw. Infusionslösung oder einer Lösung zur intravesikalen Anwendung + Pulver für ein Konzentrat zur Herstellung einer Infusionslösung + Pulver zur Herstellung einer Lösung zum Einnehmen + Pulver und Lösungsmittel zur Herstellung einer Infusionslösung + Perlongetten + Pulver und Lösungsmittel zur Herstellung einer Injektions- bzw. Infusionslösung + Pulver und Lösungsmittel zur Herstellung einer Injektionslösung + Pulver und Lösungsmittel für ein Konzentrat zur Herstellung einer Infusionslösung + Pulver und Lösungsmittel zur Herstellung einer Injektionssuspension + Pulver und Lösungsmittel zur Herstellung einer Lösung zur intravesikalen Anwendung + Pumplösung + Presslinge + Pulver zur Herstellung einer Suspension zum Einnehmen + Paste + Puder + Pulver + Retard-Dragées + Retard-Kapseln + Retard-Tabletten + Retard-Granulat + Rektalkapseln + Retardmikrokapseln und Suspensionsmittel + Rektalschaum + Rektalsuspension + Retard-überzogene-Tabletten + Saft + Salbe + Salbe zur Anwendung in der Mundhöhle + Schaum + Seife + Shampoo + Sirup + Salz + Schmelzfilm + Schmelztabletten + Suppositorien mit Mulleinlage + Spritzampullen + Sprühflasche + Spüllösung + Spray + Transdermales Spray + Spritzen + Säuglings-Suppositorien + Stechampullen + Stäbchen + Stifte + Streifen + Substanz + Suspension zum Einnehmen + Sublingualspray Lösung + Suppositorien + Suspension + Sublingualtabletten + Suspension für Vernebler + Schwämme + Tabletten + Täfelchen + Trockenampullen + Tee + Tropfen zum Einnehmen + Test + Tinktur + Tabletten in Kalenderpackung + Tablette zur Herstellung einer Lösung zum Einnehmen + Magensaftresistente Tabletten + Tonikum + Tampon + Tamponaden + Trinkampullen + Trituration + Tropfen + Trockensubstanz mit Lösungsmittel + Trinktablette + Trockensaft + Tabletten zur Herstellung einer Suspension zum Einnehmen für einen Dosierspender + Tablette zur Herstellung einer Suspension zum Einnehmen + Trockensubstanz ohne Lösungsmittel + Teststäbchen + Transdermales System + Teststreifen + Tube + Tücher + Tupfer + Tablette mit veränderter Wirkstofffreisetzung + Überzogene Tablette + Vaginallösung + Vaginalring + Vaginalcreme + Verband + Vaginalgel + Vaginalkapseln + Vlies + Vaginalovula + Vaginalstäbchen + Vaginalsuppositorien + Vaginaltabletten + Watte + Wundgaze + Weichkapseln + Magensaftresistente Hartkapseln + Würfel + Duschgel + Deo-Spray + Festiger + Gesichtsmaske + Halsband + Haarspülung + Nachtcreme + Körperpflege + Tagescreme + Zylinderampullen + Zahnbürste + Zahncreme + Zahngel + Zerbeißkapseln + Zahnpasta + diff --git a/app/features/src/main/res/values/themes.xml b/app/features/src/main/res/values/themes.xml new file mode 100644 index 00000000..af55e469 --- /dev/null +++ b/app/features/src/main/res/values/themes.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/src/main/res/xml/file_paths.xml b/app/features/src/main/res/xml/file_paths.xml similarity index 100% rename from android/src/main/res/xml/file_paths.xml rename to app/features/src/main/res/xml/file_paths.xml diff --git a/app/features/src/main/res/xml/network_security_config.xml b/app/features/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..2370e220 --- /dev/null +++ b/app/features/src/main/res/xml/network_security_config.xml @@ -0,0 +1,63 @@ + + + + erp-ref.app.ti-dienste.de + + RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= + e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= + qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= + + + + + erp-test.app.ti-dienste.de + + RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= + e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= + qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= + + + + + erp.app.ti-dienste.de + + RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho= + e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= + qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= + + + + + + idp-ref.app.ti-dienste.de + + 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= + OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= + + + + + idp-test.app.ti-dienste.de + + 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= + OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= + + + + + idp.app.ti-dienste.de + + 86fLIetopQLDNxFZ0uMI66Xpl1pFgLlHHn9v6kT0i4I= + OD/WDbD3VsfMwwNzzy9MWd9JXppKB77Vb3ST2wn9meg= + + + + + apovzd.app.ti-dienste.de + + e0IRz5Tio3GA1Xs4fUVWmH1xHDiH2dMbVtCBSkOIdqM= + qBRjZmOmkSNJL0p70zek7odSIzqs/muR4Jk9xYyCP+E= + + + + diff --git a/android/src/release/java/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/debug/ui/DebugScreenWrapper.kt similarity index 100% rename from android/src/release/java/de/gematik/ti/erp/app/debug/ui/DebugScreenWrapper.kt rename to app/features/src/release/kotlin/de.gematik.ti.erp.app/debug/ui/DebugScreenWrapper.kt diff --git a/android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt similarity index 100% rename from android/src/release/java/de/gematik/ti/erp/app/di/EndpointHelper.kt rename to app/features/src/release/kotlin/de.gematik.ti.erp.app/di/EndpointHelper.kt diff --git a/android/src/release/java/de/gematik/ti/erp/app/utils/compose/ReleaseCommon.kt b/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt similarity index 94% rename from android/src/release/java/de/gematik/ti/erp/app/utils/compose/ReleaseCommon.kt rename to app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt index 54121d5d..bc8ca8b1 100644 --- a/android/src/release/java/de/gematik/ti/erp/app/utils/compose/ReleaseCommon.kt +++ b/app/features/src/release/kotlin/de.gematik.ti.erp.app/utils/compose/ReleaseCommon.kt @@ -37,6 +37,6 @@ fun Modifier.visualTestTag(tag: String) = this @Composable -fun DebugOverlay(elements: Map) { +fun DebugOverlay(elements: Map) { error("Debug overlay should only be used in debug builds!") } diff --git a/app/shared-test/.gitignore b/app/shared-test/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/app/shared-test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/shared-test/build.gradle.kts b/app/shared-test/build.gradle.kts new file mode 100644 index 00000000..bbb2da70 --- /dev/null +++ b/app/shared-test/build.gradle.kts @@ -0,0 +1,114 @@ +@file:Suppress("UnstableApiUsage") + +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject +import org.owasp.dependencycheck.reporting.ReportGenerator.Format + +plugins { + id("com.android.library") + kotlin("android") + kotlin("plugin.serialization") + id("org.jetbrains.compose") + id("io.realm.kotlin") + id("kotlin-parcelize") + id("org.owasp.dependencycheck") + id("com.jaredsburrows.license") + id("de.gematik.ti.erp.dependencies") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") +} + +tasks.named("preBuild") { + dependsOn(":ktlint", ":detekt") +} + +licenseReport { + generateCsvReport = false + generateHtmlReport = false + generateJsonReport = true + copyJsonReportToAssets = true +} + +android { + namespace = "de.gematik.ti.erp.app.sharedtest" + kotlinOptions { + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET + freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } + dependencyCheck { + analyzers.assemblyEnabled = false + suppressionFile = "${project.rootDir}" + "/config/dependency-check/suppressions.xml" + formats = listOf(Format.HTML, Format.XML) + scanConfigurations = configurations.filter { + it.name.startsWith("api") || + it.name.startsWith("implementation") || + it.name.startsWith("kapt") + }.map { it.name } + } +} + +dependencies { + implementation(project(":common")) + implementation(project(mapOf("path" to ":app:features"))) + testImplementation(project(":common")) + implementation(kotlin("stdlib")) + implementation(kotlin("reflect")) + testImplementation(kotlin("test")) + + inject { + androidX { + implementation(legacySupport) + implementation(appcompat) + implementation(coreKtx) + implementation(datastorePreferences) + implementation(security) + implementation(biometric) + implementation(webkit) + + implementation(lifecycleViewmodel) + implementation(lifecycleComposeRuntime) + implementation(lifecycleProcess) + implementation(composeNavigation) + implementation(composeActivity) + implementation(composePaging) + implementation(camerax2) + implementation(cameraxLifecycle) + implementation(cameraxView) + } + logging { + implementation(napier) + } + androidXTest { + testImplementation(archCore) + implementation(core) + implementation(rules) + implementation(junitExt) + implementation(runner) + androidTestUtil(orchestrator) + androidTestUtil(services) + implementation(navigation) + implementation(espresso) + implementation(espressoIntents) + } + coroutinesTest { + testImplementation(coroutinesTest) + } + composeTest { + implementation(ui) + debugImplementation(uiManifest) + implementation(junit4) + } + test { + testImplementation(junit4) + testImplementation(snakeyaml) + testImplementation(json) + testImplementation(mockk) + androidTestImplementation(mockkAndroid) + } + } +} + +secrets { + defaultPropertiesFileName = if (project.rootProject.file("ci-overrides.properties").exists() + ) "ci-overrides.properties" else "gradle.properties" +} diff --git a/app/shared-test/consumer-rules.pro b/app/shared-test/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/app/shared-test/proguard-rules.pro b/app/shared-test/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/app/shared-test/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/shared-test/src/main/AndroidManifest.xml b/app/shared-test/src/main/AndroidManifest.xml new file mode 100644 index 00000000..10728cc7 --- /dev/null +++ b/app/shared-test/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt new file mode 100644 index 00000000..e9011e1a --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/actions/PharmacyScreenAction.kt @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.sharedtest.testresources.actions + +import androidx.compose.ui.test.junit4.ComposeTestRule +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig +import de.gematik.ti.erp.app.sharedtest.testresources.screens.MainScreen +import de.gematik.ti.erp.app.sharedtest.testresources.screens.OnboardingScreen +import de.gematik.ti.erp.app.sharedtest.testresources.screens.PharmacySearchScreen + + +// ZoTI - Testapotheken Pharmacies Reference: https://wiki.gematik.de/display/DEV/ZoTI+-+Testapotheken +class PharmacyScreenAction(private val composeRule: ComposeTestRule) { + + private val onboardingScreen by lazy { OnboardingScreen(composeRule) } + private val mainScreen by lazy { MainScreen(composeRule) } + private val pharmacySearchScreen by lazy { PharmacySearchScreen(composeRule) } + + /** + * For Zoti Pharmacy -> 2 + * PickupService -> Enabled + * */ + fun pickupServiceSuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti02) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy -> 1,5 + * PickupService -> Disabled + * */ + fun pickupServiceFailTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti01) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti05) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + /** + * For Zoti Pharmacy -> 3 + * CourierDelivery -> Enabled + * */ + fun courierDeliverySuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti03) + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy -> 6 + * CourierDelivery > Disabled + * */ + fun courierDeliveryFailTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti06) + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + /** + * For Zoti Pharmacy -> 7 + * MailDelivery > Disabled + * */ + fun mailDeliveryFailTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti07) + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + /** + * For Zoti Pharmacy -> 12 + * PickupService -> Disabled + * MailDelivery > Disabled + * CourierDelivery > Disabled + * */ + fun pickupServiceMailDeliveryCourierDeliveryFailTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti12) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + pharmacySearchScreen.awaitOrderOptionsEnabled() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + pharmacySearchScreen.awaitOrderOptionsEnabled() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + /** + * For Zoti Pharmacy -> 14, 15 + * PickupService -> Enabled + * MailDelivery > Enabled + * */ + fun pickupServiceMailDeliverySuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti14) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti15) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy ->4, 16 + * MailDelivery > Enabled + * */ + fun mailDeliverySuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti04) + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti16) + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy -> 13, 17 + * PickupService -> Enabled + * CourierDelivery > Enabled + * */ + fun pickupServiceCourierSuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti13) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti17) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + /** + * For Zoti Pharmacy -> 8, 9, 10, 11 & 18 + * PickupService -> Enabled + * MailDelivery > Enabled + * CourierDelivery > Enabled + * */ + fun pickupServiceMailDeliveryCourierDeliverySuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti08) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti09) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti10) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti11) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + + pharmacySearchScreen.dismissOrderOptionsBottomSheet() + + goToPharmacyOrderOptionsAfterBottomSheetDismissed(TestConfig.PharmacyZoti18) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy -> 19 + * MailDelivery > Enabled + * CourierDelivery > Enabled + * */ + fun mailDeliveryCourierDeliverySuccessTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti19) + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + pharmacySearchScreen.userClicksOnOrderByCourierDelivery() + pharmacySearchScreen.checkAndClickNoPrescriptionDialog() + } + + /** + * For Zoti Pharmacy -> 20 + * PickupService -> Disabled + * MailDelivery > Disabled + * */ + fun pickupServiceMailDeliveryFailTest() { + navigateToPharmacyOrderOptions(TestConfig.PharmacyZoti20) + pharmacySearchScreen.userClicksOnOrderByPickUp() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + pharmacySearchScreen.awaitOrderOptionsEnabled() + pharmacySearchScreen.userClicksOnOrderByMailDelivery() + pharmacySearchScreen.checkToastMessageWhenOrderOptionClicked() + } + + private fun navigateToPharmacyOrderOptions(pharmacyName: String) { + onboardingScreen.tapSkipOnboardingButton() + mainScreen.openPharmaciesFromBottomBarFromStart() + pharmacySearchScreen.openOrderOptionsAndClickSearchButton() + pharmacySearchScreen.searchPharmacyByName(pharmacyName) + } + + private fun goToPharmacyOrderOptionsAfterBottomSheetDismissed(pharmacyName: String) { + pharmacySearchScreen.userClicksOnPharmacyFromListByName(pharmacyName) + pharmacySearchScreen.userSeesPharmacyOrderOptions() + pharmacySearchScreen.awaitOrderOptionsEnabled() + } +} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt new file mode 100644 index 00000000..5ecd3ec6 --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/config/TestConfig.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("ktlint:max-line-length") + +package de.gematik.ti.erp.app.sharedtest.testresources.config + +object TestConfig { + const val WeakPassword = "TrustNo1" + const val StrongPassword = "Jaja Ding Dong!" + const val DefaultProfileName = "Rainer Reizdarm" + const val DefaultEGKCAN = "123123" + const val DefaultEGKPassword = "123456" + const val WaitTimeout1MilliSec = 100L + const val WaitTimeout1Sec = 1_000L + const val ScreenChangeTimeout = 3_000L + const val WaitTimeout5Sec = 5_000L + const val LoadPrescriptionsTimeout = 20_000L + + const val AppDefaultVirtualEgkKvnr = "X764228532" + const val PharmacyName = "Apotheke am Flughafen - E2E-Test" + const val PharmacyTelematikId = "3-SMC-B-Testkarte-883110000116873" + + const val PharmacyZoti = "ZoTI" + const val PharmacyZoti01 = "ZoTI_01_TEST-ONLY" + const val PharmacyZoti02 = "ZoTI_02_TEST-ONLY" + const val PharmacyZoti03 = "ZoTI_03_TEST-ONLY" + const val PharmacyZoti04 = "ZoTI_04_TEST-ONLY" + const val PharmacyZoti05 = "ZoTI_05_TEST-ONLY" + const val PharmacyZoti06 = "ZoTI_06_TEST-ONLY" + const val PharmacyZoti07 = "ZoTI_07_TEST-ONLY" + const val PharmacyZoti08 = "ZoTI_08_TEST-ONLY" + const val PharmacyZoti09 = "ZoTI_09_TEST-ONLY" + const val PharmacyZoti10 = "ZoTI_10_TEST-ONLY" + const val PharmacyZoti11 = "ZoTI_11_TEST-ONLY" + const val PharmacyZoti12 = "ZoTI_12_TEST-ONLY" + const val PharmacyZoti13 = "ZoTI_13_TEST-ONLY" + const val PharmacyZoti14 = "ZoTI_14_TEST-ONLY" + const val PharmacyZoti15 = "ZoTI_15_TEST-ONLY" + const val PharmacyZoti16 = "ZoTI_16_TEST-ONLY" + const val PharmacyZoti17 = "ZoTI_17_TEST-ONLY" + const val PharmacyZoti18 = "ZoTI_18_TEST-ONLY" + const val PharmacyZoti19 = "ZoTI_19_TEST-ONLY" + const val PharmacyZoti20 = "ZoTI_20_TEST-ONLY" + + object FD { + const val DefaultServer = "https://erpps-test.dev.gematik.solutions" + const val DefaultDoctor = "9a15b6f9f4b8f2e9df1db745a4091bbd" + const val DefaultPharmacy = "886c6eda7dd5a1c6b1d112907f544d3" + } +} + +interface VirtualEgk { + val certificate: String + val privateKey: String + val kvnr: String + val name: String +} + +object VirtualEgk1 : VirtualEgk { + @Suppress("MaxLineLength") + override val certificate = + "MIIDXTCCAwSgAwIBAgIHAs9vZEwB8jAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAxMjQwMDAwMDBaFw0yNzAxMjMyMzU5NTlaMIHgMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDU5Mjk3MTEUMBIGA1UEBAwLVsOzcm13aW5rZWwxHDAaBgNVBCoME1hlbmlhIFZlcmEgQWRlbGhlaWQxEjAQBgNVBAwMCVByb2YuIERyLjE1MDMGA1UEAwwsUHJvZi4gRHIuIFhlbmlhIFZlcmEgQS4gVsOzcm13aW5rZWxURVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAEczsMfajcnKpGYyNeXUhODjyrX4z4j9Qzio/Ulq5COPVySk0CxYBDj+1VEd5FalhEJXC9HjVRCflQx+RkEQFbvqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBTCDfBZ8X30CZnFk7E2x8+lMM5uODA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIDDAXcyOKDYOZpoH0iYijr1yisyxHeT3ch6XZlFNXPrKAiAHepW4TOQAoqyoGG9Pgly0TO2tTB7WLKEc7B3F6lNhpA==" + override val privateKey = "AJzshqeIuhwReqZpWbqY0PnRjTdTRzk4Zj9GpSxcUukA" + override val kvnr = "X110592971" + override val name = "Vórmwinkel Xenia Vera Adelheid" +} + +object VirtualEgkWithPrescription : VirtualEgk { + @Suppress("MaxLineLength") + override val certificate = + "MIIDLTCCAtSgAwIBAgIHAZ/zfVKUfTAKBggqhkjOPQQDAjCBljELMAkGA1UEBhMCREUxHzAdBgNVBAoMFmdlbWF0aWsgR21iSCBOT1QtVkFMSUQxRTBDBgNVBAsMPEVsZWt0cm9uaXNjaGUgR2VzdW5kaGVpdHNrYXJ0ZS1DQSBkZXIgVGVsZW1hdGlraW5mcmFzdHJ1a3R1cjEfMB0GA1UEAwwWR0VNLkVHSy1DQTEwIFRFU1QtT05MWTAeFw0yMjAyMDQwMDAwMDBaFw0yNzAyMDMyMzU5NTlaMIGwMQswCQYDVQQGEwJERTEpMCcGA1UECgwgZ2VtYXRpayBNdXN0ZXJrYXNzZTFHS1ZOT1QtVkFMSUQxEjAQBgNVBAsMCTk5OTU2Nzg5MDETMBEGA1UECwwKWDExMDUzNTU0MTEOMAwGA1UEBAwFS2zDtm4xDTALBgNVBCoMBEx1Y2ExDDAKBgNVBAwMA0RyLjEgMB4GA1UEAwwXRHIuIEx1Y2EgS2zDtm5URVNULU9OTFkwWjAUBgcqhkjOPQIBBgkrJAMDAggBAQcDQgAETn/MKYxsnBH9khicaXG3mFc5v4RoL0ILuJ3TreTsiFsv91OA6Yj/O4EAxm6dCpPtGgWRyVUYbOgDkaDurSUPpqOB7zCB7DAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESxTAFYVB7c2Te+5LI/Km6kXIkdMCAGA1UdIAQZMBcwCgYIKoIUAEwEgSMwCQYHKoIUAEwERjAwBgUrJAgDAwQnMCUwIzAhMB8wHTAQDA5WZXJzaWNoZXJ0ZS8tcjAJBgcqghQATAQxMB0GA1UdDgQWBBRhIkfxtBhE+Z3fcu+OWu/3gnnYqjA4BggrBgEFBQcBAQQsMCowKAYIKwYBBQUHMAGGHGh0dHA6Ly9laGNhLmdlbWF0aWsuZGUvb2NzcC8wDgYDVR0PAQH/BAQDAgeAMAoGCCqGSM49BAMCA0cAMEQCIGHDnSVg2A9NmFPhtzo4dL3CVbN94k3NrYhXLOZoCUFXAiBlE6TfW6uL91jhv8JuupHhr7X6B9YcbVizWoMxo1grFA==" + override val privateKey = "cv2z1KGMJi+M5foz3GCz0bi5pSdBIjVTqw2cUuIsJcY=" + override val kvnr = "X110535541" + override val name = "Klön Luca" +} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt new file mode 100644 index 00000000..14cbb7a9 --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/MainScreen.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.sharedtest.testresources.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import androidx.compose.ui.test.swipeUp +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout5Sec +import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay + +@Suppress("TooManyFunctions") +class MainScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesMainScreen(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreen) + } + + fun userSeesBottomSheet(timeoutMillis: Long = TestConfig.ScreenChangeTimeout) { + composeRule.awaitDisplay(timeoutMillis, TestTag.Main.MainScreenBottomSheet.Modal) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.Modal).performTouchInput { swipeUp() } + } + + fun checkProfileHasState(profileName: String, profileState: String) { + onNodeWithText(profileName, substring = true) + .assertIsDisplayed() + onNodeWithText(profileState, substring = true) + .assertIsDisplayed() + } + + fun refreshMainScreenBySwipe() { + onNodeWithTag(TestTag.Main.MainScreen) + .assertIsDisplayed() + .performTouchInput { swipeDown() } + } + + fun tapLoginButton() { + onNodeWithTag(TestTag.Main.LoginButton) + .assertIsDisplayed() + .performClick() + } + + fun tapConnectLater() { + userSeesBottomSheet(WaitTimeout5Sec) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ConnectLaterButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapTooltips() { + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + onRoot().performClick() + } + + fun tapSettingsButton() { + onNodeWithTag(TestTag.BottomNavigation.SettingsButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksBottomBarPrescriptions() { + onNodeWithTag(TestTag.BottomNavigation.PrescriptionButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksBottomBarPharmacy() { + onNodeWithTag(TestTag.BottomNavigation.PharmaciesButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun userClicksPharmacySearchBar() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + TestConfig.ScreenChangeTimeout + } + + fun userClicksBottomBarOrders() { + onNodeWithTag(TestTag.BottomNavigation.OrdersButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapAddProfileButton() { + onNodeWithTag(TestTag.Main.AddProfileButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun enterProfileName(name: String) { + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.ProfileNameField) + .assertIsDisplayed() + .performClick() + .performTextInput(name) + } + + fun tapNewProfileConfirmButton() { + userSeesBottomSheet(WaitTimeout5Sec) + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } + + fun tapCancelAddProfileButton() { + userSeesBottomSheet() + onNodeWithTag(TestTag.Main.LoginButton) // BottomSheet has no CancelButton + .performClick() + } + + fun assertConfirmationCanNotBeClicked() { + onNodeWithTag(TestTag.Main.MainScreenBottomSheet.SaveProfileNameButton) + .assertIsNotEnabled() + } + + /** + * cancel user login + * click-though all tool-tips + * click on pharmacies bottom button + */ + fun openPharmaciesFromBottomBarFromStart() { + tapConnectLater() + tapTooltips() + userClicksBottomBarPharmacy() + } +} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt new file mode 100644 index 00000000..dd044683 --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/OnboardingScreen.kt @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.sharedtest.testresources.screens + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.assertIsToggleable +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.test.swipeUp +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout5Sec +import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay + +@Suppress("TooManyFunctions") +class OnboardingScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun checkTutorialIsNotPresent() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertDoesNotExist() + } + + fun waitForSecondOnboardingPage() { + composeRule.awaitDisplay(WaitTimeout5Sec, TestTag.Onboarding.DataTermsScreen) + } + + fun waitForAnalyticsPage() { + composeRule.awaitDisplay(WaitTimeout5Sec, TestTag.Onboarding.Analytics.ScreenContent) + } + + fun tapContinueButton() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .performClick() + } + + fun switchToPasswordMode() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordTab) + .assertIsDisplayed() + .performClick() + } + + fun enterPasswordA(password: String) { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldA) + .assertIsDisplayed() + .performClick() + .assertIsFocused() + .performTextInput(password) + } + + fun enterPasswordB(password: String) { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordFieldB) + .assertIsDisplayed() + .performClick() + .assertIsFocused() + .performTextInput(password) + } + + fun tapDataTermsSwitch() { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .performClick() + } + + fun closeDataProtection() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun checkDataProtectionIsDisplayed() { + onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) + .assertIsDisplayed() + } + + fun openDataProtection() { + onNodeWithTag(TestTag.Onboarding.DataTerms.OpenDataProtectionButton) + .assertIsDisplayed() + .performClick() + } + + fun checkDataProtectionAreNotDisplayed() { + onNodeWithTag(TestTag.Onboarding.DataProtectionScreen) + .assertDoesNotExist() + } + + fun closeTermsOfUse() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun openTermsOfUse() { + onNodeWithTag(TestTag.Onboarding.DataTerms.OpenTermsOfUseButton) + .assertIsDisplayed() + .performClick() + } + + fun checkTermsOfUseAreDisplayed() { + onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) + .assertIsDisplayed() + } + + fun checkTermsOfUseAreNotDisplayed() { + onNodeWithTag(TestTag.Onboarding.TermsOfUseScreen) + .assertDoesNotExist() + } + + fun checkNoPasswordErrorMessagePresent() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) + .assertIsDisplayed() + .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "sufficient")) + } + + fun checkPasswordErrorMessagePresent() { + onNodeWithTag(TestTag.Onboarding.Credentials.PasswordStrengthCheck) + .assertIsDisplayed() + .assert(SemanticsMatcher.expectValue(SemanticsProperties.StateDescription, "insufficient")) + } + + fun checkContinueTutorialButtonIsEnabled() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsEnabled() + } + + fun checkContinueTutorialButtonIsDeactivated() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsNotEnabled() + } + + fun checkDataTermsSwitchDeactivated() { + onNodeWithTag(TestTag.Onboarding.DataTerms.AcceptDataTermsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOff() + } + + fun checkWelcomePageIsPresent() { + onNodeWithTag(TestTag.Onboarding.WelcomeScreen) + .assertExists() + } + + fun checkCredentialsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.CredentialsScreen) + .assertExists() + } + + fun checkAnalyticsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.AnalyticsScreen) + .assertExists() + } + + fun checkDataTermsPageIsPresent() { + onNodeWithTag(TestTag.Onboarding.DataTermsScreen) + .assertExists() + } + + fun swipeToNextTutorialStep() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertIsDisplayed() + .performTouchInput { swipeLeft() } + } + + fun swipeToPreviousTutorialStep() { + onNodeWithTag(TestTag.Onboarding.Pager) + .assertIsDisplayed() + .performTouchInput { swipeRight() } + } + + fun checkContinueTutorialButtonIsDisabled() { + onNodeWithTag(TestTag.Onboarding.NextButton) + .assertIsDisplayed() + .assertIsNotEnabled() + } + + fun tapSkipOnboardingButton() { + onNodeWithTag(TestTag.Onboarding.SkipOnboardingButton) + .assertIsDisplayed() + .performClick() + } + + fun checkAnalyticsSwitchIsDeactivated() { + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .performScrollTo() + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOff() + } + + fun checkAnalyticsSwitchIsActivated() { + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .performScrollTo() + .assertIsDisplayed() + .assertIsToggleable() + .assertIsOn() + } + + fun toggleAnalyticsSwitch() { + onNodeWithTag(TestTag.Onboarding.ScreenContent) + .performTouchInput { + swipeUp() + } + + onNodeWithTag(TestTag.Onboarding.AnalyticsSwitch) + .assertIsDisplayed() + .assertIsToggleable() + .performClick() + } + + fun tapAcceptAnalyticsButton() { + onNodeWithTag(TestTag.Onboarding.Analytics.AcceptAnalyticsButton) + .assertIsDisplayed() + .assertHasClickAction() + .performClick() + } +} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt new file mode 100644 index 00000000..b71346e1 --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/screens/PharmacySearchScreen.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.sharedtest.testresources.screens + +import android.view.KeyEvent +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isSelected +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToKey +import androidx.compose.ui.test.performScrollToNode +import androidx.compose.ui.test.performTextInput +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions.pressKey +import androidx.test.espresso.matcher.ViewMatchers +import de.gematik.ti.erp.app.PrescriptionIds +import de.gematik.ti.erp.app.TestTag +import de.gematik.ti.erp.app.prescription.usecase.model.PrescriptionUseCaseData +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.LoadPrescriptionsTimeout +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout1Sec +import de.gematik.ti.erp.app.sharedtest.testresources.utils.awaitDisplay +import de.gematik.ti.erp.app.sharedtest.testresources.utils.hasPharmacyId +import de.gematik.ti.erp.app.sharedtest.testresources.utils.hasPrescriptionId +import de.gematik.ti.erp.app.sharedtest.testresources.utils.sleep + +@Suppress("TooManyFunctions") +class PharmacySearchScreen(private val composeRule: ComposeTestRule) : + SemanticsNodeInteractionsProvider by composeRule { + + fun userSeesPharmacySearchResultScreen() { + onNodeWithTag(TestTag.PharmacySearch.ResultScreen) + .assertIsDisplayed() + } + + fun awaitSearchResults() { + composeRule.awaitDisplay(LoadPrescriptionsTimeout) { + onNodeWithTag(TestTag.PharmacySearch.ResultContent) + .assertIsDisplayed() + .assert(hasAnyChild(hasTestTag(TestTag.PharmacySearch.PharmacyListEntry))) + } + composeRule.sleep(WaitTimeout1Sec) + } + + fun userClicksOnTestPharmacy() { + onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) + .assertIsDisplayed() + .performScrollToNode(hasPharmacyId(TestConfig.PharmacyTelematikId)) + .onChildren() + .filterToOne(hasPharmacyId(TestConfig.PharmacyTelematikId)) + .performClick() + } + + fun userClicksOnPharmacyFromListByName(name: String) { + composeRule.sleep(WaitTimeout1Sec) + onNodeWithTag(TestTag.PharmacySearch.ResultContent, useUnmergedTree = true) + .performScrollToNode(hasAnyDescendant(hasText(name))) + .onChildren() + .filterToOne(hasAnyDescendant(hasText(name))) + .performClick() + } + + fun userSeesPharmacyOrderOptions() { + composeRule.sleep(WaitTimeout1Sec) + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.Content, useUnmergedTree = true) + .assertExists() + } + + fun awaitOrderOptionsEnabled() { + composeRule.sleep(WaitTimeout1Sec) + } + + fun dismissOrderOptionsBottomSheet() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchField) + .assertIsDisplayed() + .performClick() + } + + fun userClicksOnOrderByCourierDelivery() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.CourierDeliveryOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun userClicksOnOrderByPickUp() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.PickUpOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun checkToastMessageWhenOrderOptionClicked() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.ComposeToast, useUnmergedTree = true) + .assertExists() + } + + fun checkAndClickNoPrescriptionDialog() { + onNodeWithTag(TestTag.AlertDialog.ConfirmButton).assertIsDisplayed().performClick() + } + + fun userClicksOnOrderByMailDelivery() { + onNodeWithTag(TestTag.PharmacySearch.OrderOptions.MailDeliveryOptionButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun userSeesPharmacyOrderSummaryScreen() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.Screen) + .assertIsDisplayed() + } + + fun userSeesSendOrderButtonEnabled() { + // asynchronous process enabling the button + composeRule.awaitDisplay(WaitTimeout1Sec) { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) + .assertIsDisplayed() + .assertIsEnabled() + } + } + + fun userClicksPrescriptionSelection() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.PrescriptionSelectionButton) + .assertIsDisplayed() + .performClick() + } + + fun userSeesPrescriptionSelectionScreen() { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Screen) + .assertIsDisplayed() + } + + fun userDeselectsAllPrescriptions() { + val prescriptionIds = onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .fetchSemanticsNode() + .config[PrescriptionIds]!! + + prescriptionIds.forEach { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .performScrollToKey("prescription-$it") + .onChildren() + .filterToOne(hasPrescriptionId(it).and(isSelected())) + .performClick() + } + } + + fun userSelectsPrescription(prescription: PrescriptionUseCaseData.Prescription) { + onNodeWithTag(TestTag.PharmacySearch.OrderPrescriptionSelection.Content) + .assertIsDisplayed() + .performScrollToKey("prescription-${prescription.taskId}") + .onChildren() + .filterToOne(hasPrescriptionId(prescription.taskId)) + .assertIsNotSelected() + .assertIsDisplayed() + .performClick() + .assertIsSelected() + } + + fun userClicksBack() { + onNodeWithTag(TestTag.TopNavigation.BackButton) + .assertIsDisplayed() + .performClick() + } + + fun userClicksSendOrderButton() { + onNodeWithTag(TestTag.PharmacySearch.OrderSummary.SendOrderButton) + .assertIsDisplayed() + .assertIsEnabled() + .performClick() + } + + fun userSearchesForTestPharmacy() { + onNodeWithTag(TestTag.PharmacySearch.TextSearchField) + .performClick() + .assertIsFocused() + .performTextInput(TestConfig.PharmacyZoti) + + Espresso.onView(ViewMatchers.isRoot()).perform(pressKey(KeyEvent.KEYCODE_ENTER)) + } + + fun openOrderOptionsAndClickSearchButton() { + onNodeWithTag(TestTag.PharmacySearch.OverviewScreen, useUnmergedTree = true) + .assertIsDisplayed() + onNodeWithTag(TestTag.PharmacySearch.TextSearchButton) + .performClick() + } + + fun searchPharmacyByName(name: String) { + userSearchesForTestPharmacy() + awaitSearchResults() + userClicksOnPharmacyFromListByName(name) + userSeesPharmacyOrderOptions() + awaitOrderOptionsEnabled() + } +} diff --git a/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt new file mode 100644 index 00000000..225fd0c4 --- /dev/null +++ b/app/shared-test/src/main/kotlin/de/gematik/ti/erp/app/sharedtest/testresources/utils/Util.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.sharedtest.testresources.utils + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.semantics.getOrNull +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.filter +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.printToString +import androidx.test.platform.app.InstrumentationRegistry +import de.gematik.ti.erp.app.InsuranceState +import de.gematik.ti.erp.app.MedicationCategory +import de.gematik.ti.erp.app.PharmacyId +import de.gematik.ti.erp.app.PrescriptionId +import de.gematik.ti.erp.app.SubstitutionAllowed +import de.gematik.ti.erp.app.SupplyForm +import de.gematik.ti.erp.app.sharedtest.testresources.config.TestConfig.WaitTimeout1MilliSec + +fun ComposeTestRule.awaitDisplay(timeout: Long, vararg tags: String): String { + val t0 = System.currentTimeMillis() + do { + tags.forEach { tag -> + try { + onNodeWithTag(tag).assertIsDisplayed() + return tag + } catch (_: AssertionError) { + } + } + mainClock.advanceTimeBy(WaitTimeout1MilliSec) + Thread.sleep(WaitTimeout1MilliSec) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.awaitDisplay(timeout: Long, node: () -> SemanticsNodeInteraction) { + val t0 = System.currentTimeMillis() + do { + try { + node().assertIsDisplayed() + return + } catch (_: AssertionError) { + } + mainClock.advanceTimeBy(WaitTimeout1MilliSec) + Thread.sleep(WaitTimeout1MilliSec) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.await(timeout: Long, node: () -> Unit) { + val t0 = System.currentTimeMillis() + do { + try { + node() + return + } catch (_: AssertionError) { + } + mainClock.advanceTimeBy(WaitTimeout1MilliSec) + Thread.sleep(WaitTimeout1MilliSec) + } while (System.currentTimeMillis() - t0 < timeout) + throw AssertionError( + "Node was not displayed after $timeout milliseconds. Root node was:\n${ + onRoot().printToString(Int.MAX_VALUE) + }" + ) +} + +fun ComposeTestRule.sleep(timeout: Long) { + val t0 = System.currentTimeMillis() + do { + mainClock.advanceTimeBy(WaitTimeout1MilliSec) + Thread.sleep(WaitTimeout1MilliSec) + } while (System.currentTimeMillis() - t0 < timeout) +} + +fun SemanticsNodeInteraction.assertHasText(includeEditableText: Boolean = true) = + assert(hasText(includeEditableText)) + +fun hasText( + includeEditableText: Boolean = true +): SemanticsMatcher { + val propertyName = if (includeEditableText) { + "${SemanticsProperties.Text.name} + ${SemanticsProperties.EditableText.name}" + } else { + SemanticsProperties.Text.name + } + return SemanticsMatcher( + propertyName + ) { node -> + val actual = mutableListOf() + if (includeEditableText) { + node.config.getOrNull(SemanticsProperties.EditableText) + ?.let { actual.add(it.text) } + } + node.config.getOrNull(SemanticsProperties.Text) + ?.let { actual.addAll(it.map { anStr -> anStr.text }) } + actual.all { it.isNotBlank() } + } +} + +fun SemanticsNodeInteractionCollection.assertNone( + matcher: SemanticsMatcher +): SemanticsNodeInteractionCollection = + filter(matcher) + .assertCountEquals(0) + +fun hasPrescriptionId(id: String): SemanticsMatcher = + SemanticsMatcher.expectValue(PrescriptionId, id) + +fun hasPharmacyId(id: String): SemanticsMatcher = + SemanticsMatcher.expectValue(PharmacyId, id) + +fun hasInsuranceState(state: String?): SemanticsMatcher = + SemanticsMatcher.expectValue(InsuranceState, state) + +fun hasSubstitutionAllowed(allowed: Boolean): SemanticsMatcher = + SemanticsMatcher.expectValue(SubstitutionAllowed, allowed) + +fun hasSupplyForm(form: String): SemanticsMatcher = + SemanticsMatcher.expectValue(SupplyForm, form) + +fun hasMedicationCategory(form: String): SemanticsMatcher = + SemanticsMatcher.expectValue(MedicationCategory, form) + +fun execShellCmd(cmd: String) { + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand(cmd) +} diff --git a/build.gradle.kts b/build.gradle.kts index 8bfc7d11..675954b3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,39 +1,46 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask -buildscript { - dependencies { - classpath("com.karumi:shot:6.0.0") - } -} // NOTE: Only pre-include plugins (apply false) required by the modules android, common // and desktop within this block to keep them excluded from the root module. // If the plugin can't be resolved add a custom resolution strategy to `settings.gradle.kts`. plugins { - // reports versions of dependencies - // e.g. `gradle dependencyUpdates` - id("com.github.ben-manes.versions") version "0.45.0" + + // Running `./gradlew dependencyUpdates` looks for the latest libs that are available + id("com.github.ben-manes.versions") version "0.48.0" id("org.owasp.dependencycheck") version "8.0.2" apply false // generates licence report id("com.jaredsburrows.license") version "0.8.90" apply false - kotlin("multiplatform") version "1.8.10" apply false + id("com.android.application") version "8.1.0" apply false + + id("com.android.library") version "8.1.0" apply false + + kotlin("multiplatform") version "1.9.10" apply false + kotlin("plugin.serialization") version "1.8.10" apply false + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false + id("io.realm.kotlin") version "1.7.1" apply false - id("org.jetbrains.kotlin.android") version "1.8.10" apply false - id("com.android.application") version "7.4.1" apply false - id("com.android.library") version "7.4.1" apply false - id("org.jetbrains.compose") version "1.4.0" apply false + + // TODO: Update to latest version : https://github.com/JetBrains/compose-multiplatform/blob/master/VERSIONING.md + id("org.jetbrains.kotlin.android") version "1.9.10" apply false + + id("org.jetbrains.compose") version "1.5.3" apply false + id("com.codingfeline.buildkonfig") version "0.13.3" apply false + id("io.gitlab.arturbosch.detekt") version "1.22.0" + id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") + id("org.jetbrains.kotlin.jvm") version "1.9.0" apply false } -val ktlintMain by configurations.creating -val ktlintRules by configurations.creating +val ktlintMain: Configuration by configurations.creating +val ktlintRules: Configuration by configurations.creating dependencies { ktlintMain("com.pinterest:ktlint:0.46.1") { @@ -45,7 +52,8 @@ dependencies { } val sourcesKt = listOf( - "android/src/**/de/gematik/**/*.kt", + "app/android/src/**/de/gematik/**/*.kt", + "app/features/src/**/de/gematik/**/*.kt", "common/src/**/de/gematik/**/*.kt", "desktop/src/**/de/gematik/**/*.kt", "rules/src/**/de/gematik/**/*.kt", @@ -100,8 +108,8 @@ tasks.register("clean", Delete::class) { fun isUnstable(version: String): Boolean = version.contains("alpha", ignoreCase = true) - || version.contains("rc", ignoreCase = true) - || version.contains("beta", ignoreCase = true) + || version.contains("rc", ignoreCase = true) + || version.contains("beta", ignoreCase = true) tasks.withType { outputFormatter = "txt,html" diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 4c7898b0..c5ad647a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -2,9 +2,9 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.INT import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.LONG import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING -import de.gematik.ti.erp.app +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject import de.gematik.ti.erp.overriding -import org.jetbrains.compose.compose import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly import java.io.ByteArrayOutputStream @@ -18,7 +18,6 @@ plugins { id("de.gematik.ti.erp.dependencies") id("de.gematik.ti.erp.gradleplugins.TechnicalRequirementsPlugin") } - fun getGitHash() = if (File("${rootDir.path}/.git").exists()) { val stdout = ByteArrayOutputStream() @@ -30,23 +29,18 @@ fun getGitHash() = } else { "n/a" } - val USER_AGENT: String by overriding() val DATA_PROTECTION_LAST_UPDATED: String by overriding() - val VERSION_CODE: String by overriding() val VERSION_NAME: String by overriding() - val DEBUG_TEST_IDS_ENABLED: String by overriding() val VAU_OCSP_RESPONSE_MAX_AGE: String by overriding() - val APP_TRUST_ANCHOR_BASE64: String by overriding() val APP_TRUST_ANCHOR_BASE64_TEST: String by overriding() val PHARMACY_SERVICE_URI: String by overriding() val PHARMACY_SERVICE_URI_TEST: String by overriding() val PHARMACY_API_KEY: String by overriding() val PHARMACY_API_KEY_TEST: String by overriding() - val BASE_SERVICE_URI_PU: String by overriding() val BASE_SERVICE_URI_TU: String by overriding() val BASE_SERVICE_URI_RU: String by overriding() @@ -57,7 +51,6 @@ val IDP_SERVICE_URI_TU: String by overriding() val IDP_SERVICE_URI_RU: String by overriding() val IDP_SERVICE_URI_RU_DEV: String by overriding() val IDP_SERVICE_URI_TR: String by overriding() - val ERP_API_KEY_GOOGLE_PU: String by overriding() val ERP_API_KEY_GOOGLE_TU: String by overriding() val ERP_API_KEY_GOOGLE_RU: String by overriding() @@ -69,35 +62,36 @@ val ERP_API_KEY_HUAWEI_TR: String by overriding() val ERP_API_KEY_DESKTOP_PU: String by overriding() val ERP_API_KEY_DESKTOP_TU: String by overriding() val ERP_API_KEY_DESKTOP_RU: String by overriding() - val INTEGRITY_API_KEY: String by overriding() val INTEGRITY_VERIFICATION_KEY: String by overriding() val CLOUD_PROJECT_NUMBER: String by overriding() val DEFAULT_VIRTUAL_HEALTH_CARD_CERTIFICATE: String by overriding() val DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY: String by overriding() - val DEBUG_VISUAL_TEST_TAGS: String? by project - kotlin { - android() - jvm("desktop") { + androidTarget { compilations.all { - kotlinOptions.jvmTarget = "17" + kotlinOptions { + jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET + } } } + jvm("desktop") sourceSets { val commonMain by getting { dependencies { implementation(kotlin("reflect")) - app { + inject { androidX { - implementation(paging("common-ktx")) { + implementation(multiplatformPaging) { // remove coroutine dependency; otherwise intellij will be confused with "duplicated class import" exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") } } - kotlinX { - implementation(coroutines("core")) + coroutines { + implementation(coroutinesCore) + } + dateTime { implementation(datetime) } database { @@ -105,8 +99,8 @@ kotlin { } crypto { implementation(jose4j) - compileOnly(bouncyCastle("bcprov")) - compileOnly(bouncyCastle("bcpkix")) + compileOnly(bouncycastleBcprov) + compileOnly(bouncycastleBcpkix) } serialization { implementation(kotlinXJson) @@ -115,13 +109,13 @@ kotlin { implementation(napier) } network { - implementation(retrofit2("retrofit")) - implementation(okhttp3("okhttp")) + implementation(retrofit) + implementation(okhttp3) implementation(retrofit2KotlinXSerialization) - implementation(okhttp3("logging-interceptor")) + implementation(okhttpLogging) } dependencyInjection { - implementation(kodein("di-framework-compose")) + implementation(kodeinCompose) } } implementation(compose.runtime) @@ -133,54 +127,56 @@ kotlin { } val commonTest by getting { dependencies { - implementation(kotlin("reflect")) - implementation(kotlin("test-common")) - implementation(kotlin("test")) - app { + inject { database { implementation(realm) } + coroutinesTest { + implementation(coroutinesTest) + } + serialization { + implementation(kotlinXJson) + } test { + implementation(kotlinTest) + implementation(kotlinTestCommon) + implementation(kotlinReflect) implementation(junit4) - implementation(mockk("mockk")) + implementation(mockkOld) implementation(snakeyaml) } crypto { implementation(jose4j) - implementation(bouncyCastle("bcprov")) - implementation(bouncyCastle("bcpkix")) - } - kotlinXTest { - implementation(coroutinesTest) + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) } networkTest { implementation(mockWebServer) } + dateTime { + implementation(datetime) + } } } } val androidMain by getting { dependsOn(commonMain) dependencies { - app { - android { + inject { + androidX { implementation(coreKtx) } crypto { - implementation(bouncyCastle("bcprov")) - implementation(bouncyCastle("bcpkix")) + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) } dependencyInjection { - implementation(kodein("di-framework-android-x-viewmodel")) - implementation(kodein("di-framework-android-x-viewmodel-savedstate")) + implementation(kodeinViewModel) + implementation(kodeinSavedState) } } } } - val androidTest by getting { - dependencies { - } - } val desktopMain by getting { dependsOn(commonMain) dependencies { @@ -189,48 +185,41 @@ kotlin { } val desktopTest by getting { dependencies { - app { + inject { crypto { - implementation(bouncyCastle("bcprov")) - implementation(bouncyCastle("bcpkix")) + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) } } } } } } - android { buildToolsVersion = "33.0.1" - compileSdk = 33 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + compileSdk = Dependencies.Versions.SdkVersions.COMPILE_SDK_VERSION defaultConfig { - minSdk = 24 - targetSdk = 33 + minSdk = Dependencies.Versions.SdkVersions.MIN_SDK_VERSION } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION + targetCompatibility = Dependencies.Versions.JavaVersion.PROJECT_JAVA_VERSION } namespace = "de.gematik.ti.erp.lib" } - enum class Platforms { Google, Huawei, Konnektathon, Desktop } - enum class Environments { PU, TU, RU, DEVRU, TR } - enum class Types { Internal, External } - buildkonfig { packageName = "de.gematik.ti.erp.app" exposeObjectWithName = "BuildKonfig" - // default config is required defaultConfigs { buildConfigField(STRING, "GIT_HASH", getGitHash()) @@ -241,7 +230,6 @@ buildkonfig { buildConfigField(STRING, "DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY", DEFAULT_VIRTUAL_HEALTH_CARD_PRIVATE_KEY) buildConfigField(STRING, "BUILD_FLAVOR", project.property("buildkonfig.flavor") as String) } - fun defaultConfigs( flavor: String, isInternal: Boolean, @@ -263,27 +251,21 @@ buildkonfig { buildConfigField(STRING, "BASE_SERVICE_URI_TU", BASE_SERVICE_URI_TU) buildConfigField(STRING, "BASE_SERVICE_URI_RU_DEV", BASE_SERVICE_URI_RU_DEV) buildConfigField(STRING, "BASE_SERVICE_URI_TR", BASE_SERVICE_URI_TR) - buildConfigField(STRING, "IDP_SERVICE_URI_PU", IDP_SERVICE_URI_PU) buildConfigField(STRING, "IDP_SERVICE_URI_TU", IDP_SERVICE_URI_TU) buildConfigField(STRING, "IDP_SERVICE_URI_RU", IDP_SERVICE_URI_RU) buildConfigField(STRING, "IDP_SERVICE_URI_RU_DEV", IDP_SERVICE_URI_RU_DEV) buildConfigField(STRING, "IDP_SERVICE_URI_TR", IDP_SERVICE_URI_TR) - buildConfigField(STRING, "PHARMACY_SERVICE_URI_PU", PHARMACY_SERVICE_URI) buildConfigField(STRING, "PHARMACY_SERVICE_URI_RU", PHARMACY_SERVICE_URI_TEST) - buildConfigField(STRING, "ERP_API_KEY_GOOGLE_PU", ERP_API_KEY_GOOGLE_PU) buildConfigField(STRING, "ERP_API_KEY_GOOGLE_RU", ERP_API_KEY_GOOGLE_RU) buildConfigField(STRING, "ERP_API_KEY_GOOGLE_TU", ERP_API_KEY_GOOGLE_TU) buildConfigField(STRING, "ERP_API_KEY_GOOGLE_TR", ERP_API_KEY_GOOGLE_TR) - buildConfigField(STRING, "PHARMACY_API_KEY_PU", PHARMACY_API_KEY) buildConfigField(STRING, "PHARMACY_API_KEY_RU", PHARMACY_API_KEY_TEST) - 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) @@ -293,10 +275,8 @@ buildkonfig { buildConfigField(STRING, "PHARMACY_API_KEY", pharmacyServiceApiKey) buildConfigField(STRING, "APP_TRUST_ANCHOR_BASE64", trustAnchor) buildConfigField(LONG, "VAU_OCSP_RESPONSE_MAX_AGE", ocspResponseMaxAge) - buildConfigField(BOOLEAN, "TEST_RUN_WITH_TRUSTSTORE_INTEGRATION", "false") buildConfigField(BOOLEAN, "TEST_RUN_WITH_IDP_INTEGRATION", "false") - buildConfigField( STRING, "IDP_DEFAULT_SCOPE", @@ -308,21 +288,16 @@ buildkonfig { ) } } - val platforms = Platforms.values() val environments = Environments.values() val types = Types.values() - platforms.forEach { platform -> environments.forEach { environment -> types.forEach { type -> - val plat = platform.name.toLowerCase() - val env = environment.name.toLowerCase().capitalizeAsciiOnly() - val typ = type.name.toLowerCase().capitalizeAsciiOnly() + val plat = platform.name.lowercase() + val env = environment.name.lowercase().capitalizeAsciiOnly() + val typ = type.name.lowercase().capitalizeAsciiOnly() val flavor = plat + env + typ - - println("Flavor: $flavor") - defaultConfigs( flavor = flavor, isInternal = type == Types.Internal, @@ -389,7 +364,6 @@ buildkonfig { } } } - targetConfigs { create("desktop") { buildConfigField(STRING, "USER_AGENT", USER_AGENT) @@ -397,13 +371,10 @@ buildkonfig { create("android") { buildConfigField(STRING, "USER_AGENT", USER_AGENT) buildConfigField(STRING, "DATA_PROTECTION_LAST_UPDATED", DATA_PROTECTION_LAST_UPDATED) - // test tag config buildConfigField(BOOLEAN, "DEBUG_VISUAL_TEST_TAGS", DEBUG_VISUAL_TEST_TAGS ?: "false") - // test configs buildConfigField(BOOLEAN, "DEBUG_TEST_IDS_ENABLED", DEBUG_TEST_IDS_ENABLED) - // VAU feature toggles for development buildConfigField(BOOLEAN, "VAU_ENABLE_INTERCEPTOR", "true") } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt index 438f0631..5142e90e 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/DispatchProvider.kt @@ -21,9 +21,14 @@ package de.gematik.ti.erp.app import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +// TODO: Remove this, serves no real purpose +@Deprecated( + message = "This is an overkill, use CoroutineDispatcher", + replaceWith = ReplaceWith("kotlinx.coroutines.CoroutineDispatcher") +) interface DispatchProvider { - val Main: CoroutineDispatcher get() = Dispatchers.Main - val Default: CoroutineDispatcher get() = Dispatchers.Default - val IO: CoroutineDispatcher get() = Dispatchers.IO - val Unconfined: CoroutineDispatcher get() = Dispatchers.Unconfined + val main: CoroutineDispatcher get() = Dispatchers.Main + val default: CoroutineDispatcher get() = Dispatchers.Default + val io: CoroutineDispatcher get() = Dispatchers.IO + val unconfined: CoroutineDispatcher get() = Dispatchers.Unconfined } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt index b8a9426a..487917d0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/api/ResourcePaging.kt @@ -41,14 +41,14 @@ abstract class ResourcePaging( protected suspend fun downloadPaged(profileId: ProfileIdentifier): Result = lock.withLock { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { downloadAll(profileId) } } protected suspend fun downloadPaged(profileId: ProfileIdentifier, fold: (prev: T?, next: T) -> T): Result = lock.withLock { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { downloadAll(profileId, fold) } } @@ -61,7 +61,7 @@ abstract class ResourcePaging( while (condition && pages < maxPages) { val r = downloadResource( profileId = profileId, - timestamp = toTimestampString(syncedUpTo(profileId)), + timestamp = syncedUpTo(profileId).toTimestampString(), count = maxPageSize ).fold( onSuccess = { @@ -97,7 +97,7 @@ abstract class ResourcePaging( while (condition && pages < maxPages) { val r = downloadResource( profileId = profileId, - timestamp = toTimestampString(syncedUpTo(profileId)), + timestamp = syncedUpTo(profileId).toTimestampString(), count = maxPageSize ).fold( onSuccess = { @@ -130,16 +130,6 @@ abstract class ResourcePaging( return Result.success(result) } - private fun toTimestampString(timestamp: Instant?) = - timestamp?.let { - // TODO: remove java date time stuff - val tm = it.toJavaInstant().atOffset(ZoneOffset.UTC) - .truncatedTo(ChronoUnit.SECONDS) - .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - - "gt$tm" - } - class ResourceResult(val count: Int, val data: T) /** @@ -152,4 +142,16 @@ abstract class ResourcePaging( ): Result> protected abstract suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? + + companion object { + fun Instant?.toTimestampString() = + this?.let { + // TODO: remove java date time stuff + val tm = it.toJavaInstant().atOffset(ZoneOffset.UTC) + .truncatedTo(ChronoUnit.SECONDS) + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + "gt$tm" + } + } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/apicheck/usecase/CheckVersionUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/apicheck/usecase/CheckVersionUseCase.kt index 8ec8ea8e..bc388400 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/apicheck/usecase/CheckVersionUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/apicheck/usecase/CheckVersionUseCase.kt @@ -37,7 +37,7 @@ class CheckVersionUseCase( private val okHttp: OkHttpClient, private val dispatchers: DispatchProvider ) { - suspend fun isUpdateRequired(): Boolean = withContext(dispatchers.IO) { + suspend fun isUpdateRequired(): Boolean = withContext(dispatchers.io) { if (BuildKonfig.INTERNAL) { return@withContext false } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt new file mode 100644 index 00000000..72af9848 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/mapper/PromptAuthenticationProvider.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.mapper + +import de.gematik.ti.erp.app.authentication.model.InitialAuthenticationData +import de.gematik.ti.erp.app.authentication.model.PromptAuthenticator +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow + +interface PromptAuthenticationProvider { + fun mapAuthenticationResult( + id: ProfileIdentifier, + initialAuthenticationData: InitialAuthenticationData, + scope: PromptAuthenticator.AuthScope, + authenticators: List + ): Flow +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt new file mode 100644 index 00000000..d271f87d --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/InitialAuthenticationData.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.model + +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.profiles.usecase.model.ProfilesUseCaseData + +@Stable +sealed interface InitialAuthenticationData { + val profile: ProfilesUseCaseData.Profile +} + +data class HealthCard(val can: String, override val profile: ProfilesUseCaseData.Profile) : + InitialAuthenticationData + +data class SecureElement(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData +data class External( + val authenticatorId: String, + val authenticatorName: String, + override val profile: ProfilesUseCaseData.Profile +) : InitialAuthenticationData + +data class None(override val profile: ProfilesUseCaseData.Profile) : InitialAuthenticationData diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt new file mode 100644 index 00000000..d0314d74 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/authentication/model/PromptAuthenticator.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.authentication.model + +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow + +@Stable +interface PromptAuthenticator { + enum class AuthResult { + Authenticated, + Cancelled, + NoneEnrolled, + UserNotAuthenticated + } + + enum class AuthScope { + Prescriptions, PairedDevices + } + + fun authenticate(profileId: ProfileIdentifier, scope: AuthScope): Flow + + suspend fun cancelAuthentication() +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt index a547705c..6d01c7d6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/model/ConsentMapper.kt @@ -19,11 +19,11 @@ package de.gematik.ti.erp.app.consent.model import de.gematik.ti.erp.app.fhir.model.json -import de.gematik.ti.erp.app.utils.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 de.gematik.ti.erp.app.utils.asFhirTemporal import kotlinx.datetime.Clock import kotlinx.serialization.json.JsonElement diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt index fbff8a4e..12492fa2 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/consent/repository/ConsentRepository.kt @@ -29,7 +29,7 @@ class ConsentRepository( ) { suspend fun getConsent( profileId: ProfileIdentifier - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { remoteDataSource.getConsent( profileId = profileId ) @@ -38,7 +38,7 @@ class ConsentRepository( suspend fun grantConsent( profileId: ProfileIdentifier, consent: JsonElement - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { remoteDataSource.grantConsent( profileId = profileId, consent = consent @@ -46,7 +46,7 @@ class ConsentRepository( } suspend fun deleteChargeConsent( profileId: ProfileIdentifier - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { remoteDataSource.deleteChargeConsent( profileId = profileId ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt index 194e2820..2c3463ef 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/Migrations.kt @@ -55,7 +55,7 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmListOf import kotlinx.datetime.Instant -const val ACTUAL_SCHEMA_VERSION = 26L +const val ACTUAL_SCHEMA_VERSION = 27L val appSchemas = setOf( AppRealmSchema( @@ -166,6 +166,16 @@ val appSchemas = setOf( it.failureToReport = "" } } + + if (migrationStartedFrom < 27) { + query().find().groupBy { + it.scannedOn + }.forEach { + it.value.mapIndexed { index, scannedTaskEntityV1 -> + scannedTaskEntityV1.index = index + 1 + } + } + } } ) ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt index d00211bb..f6fa4bc6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/db/entities/v1/task/ScannedTask.kt @@ -29,6 +29,8 @@ import io.realm.kotlin.types.RealmObject class ScannedTaskEntityV1 : RealmObject, Cascading { var taskId: String = "" var accessCode: String = "" + var name: String? = null + var index: Int = 0 var scannedOn: RealmInstant = RealmInstant.MIN var redeemedOn: RealmInstant? = null var communications: RealmList = realmListOf() diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt index 628d9c1d..78128d88 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/fhir/model/PharmacyMapper.kt @@ -113,15 +113,20 @@ fun extractPharmacyServices( .containedString("value") var isMobilePharmacy = false + var isOutpatientPharmacy = false pharmacy.findAll(TypeCodingCode).forEach { when (it.containedString()) { "MOBL" -> isMobilePharmacy = true + "OUTPHARM" -> isOutpatientPharmacy = true } } - // All pharmacies offer pickup service - val pickUpPharmacyService = PickUpPharmacyService(name = locationName) + val pickUpPharmacyService = if (isOutpatientPharmacy) { + PickUpPharmacyService(name = locationName) + } else { + null + } val onlinePharmacyService = if (isMobilePharmacy) { OnlinePharmacyService(name = locationName) @@ -151,6 +156,8 @@ fun extractPharmacyServices( city = address.containedString("city") ) }, + // contacts have preference over provides! + // When Pharmacy NOT connected to TI contacts = pharmacy.containedArrayOrNull("telecom")?.let { contacts(it) } ?: PharmacyContacts( "", "", @@ -159,6 +166,7 @@ fun extractPharmacyServices( "", "" ), + // When Pharmacy connected to TI provides = listOfNotNull( localService, deliveryPharmacyService, diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt new file mode 100644 index 00000000..b6bffd56 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/AltAuthenticationCryptoException.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.idp.usecase + +class AltAuthenticationCryptoException(cause: Throwable) : IllegalStateException(cause) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt new file mode 100644 index 00000000..7464cd95 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/DefaultIdpUseCase.kt @@ -0,0 +1,668 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ +@file:Suppress("LongParameterList", "TooGenericExceptionCaught", "MagicNumber", "ThrowsCount") + +package de.gematik.ti.erp.app.idp.usecase + +import de.gematik.ti.erp.app.Requirement +import de.gematik.ti.erp.app.api.ApiCallException +import de.gematik.ti.erp.app.idp.api.EXT_AUTH_REDIRECT_URI +import de.gematik.ti.erp.app.idp.api.IdpService +import de.gematik.ti.erp.app.idp.api.REDIRECT_URI +import de.gematik.ti.erp.app.idp.api.models.AuthenticationId +import de.gematik.ti.erp.app.idp.api.models.ExternalAuthorizationData +import de.gematik.ti.erp.app.idp.api.models.IdpAuthFlowResult +import de.gematik.ti.erp.app.idp.api.models.IdpInitialData +import de.gematik.ti.erp.app.idp.api.models.IdpNonce +import de.gematik.ti.erp.app.idp.api.models.IdpScope +import de.gematik.ti.erp.app.idp.api.models.PairingData +import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository +import de.gematik.ti.erp.app.idp.repository.IdpRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository +import de.gematik.ti.erp.app.vau.extractECPublicKey +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.bouncycastle.util.encoders.Base64 +import java.net.HttpURLConnection +import java.net.URI +import java.security.KeyStore +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Signature + +class DefaultIdpUseCase( + private val repository: IdpRepository, + private val pairingRepository: IdpPairingRepository, + private val altAuthUseCase: IdpAlternateAuthenticationUseCase, + private val profilesRepository: ProfileRepository, + private val basicUseCase: IdpBasicUseCase, + private val preferences: IdpPreferenceProvider, + private val cryptoProvider: IdpCryptoProvider +) : IdpUseCase { + private val lock = Mutex() + + /** + * If no bearer token is set or [refresh] is true, this will trigger [IdpBasicUseCase.refreshAccessTokenWithSsoFlow]. + */ + @Requirement( + "A_20283-01#1", + "A_21326", + "A_21327", + sourceSpecification = "gemSpec_eRp_FdV", + rationale = "Load and decrypt access token." + ) + override suspend fun loadAccessToken( + profileId: ProfileIdentifier, + refresh: Boolean, + scope: IdpScope + ): String = lock.withLock { + when (scope) { + IdpScope.Default -> + loadAccessToken( + refresh = refresh, + profileId = profileId, + scope = IdpScope.Default, + singleSignOnTokenScope = { + repository.authenticationData(profileId).first().singleSignOnTokenScope + }, + decryptedAccessToken = { repository.decryptedAccessToken(profileId).first() }, + invalidateDecryptedAccessToken = { repository.invalidateDecryptedAccessToken(profileId) }, + invalidateSingleSignOnTokenRetainingScope = { + repository.invalidateSingleSignOnTokenRetainingScope( + profileId + ) + }, + saveDecryptedAccessToken = { repository.saveDecryptedAccessToken(profileId, it) } + ) + + IdpScope.BiometricPairing -> + loadAccessToken( + refresh = refresh, + profileId = profileId, + scope = IdpScope.BiometricPairing, + singleSignOnTokenScope = { pairingRepository.singleSignOnTokenScope(profileId).first() }, + decryptedAccessToken = { pairingRepository.decryptedAccessToken(profileId).first() }, + invalidateDecryptedAccessToken = { pairingRepository.invalidateDecryptedAccessToken(profileId) }, + invalidateSingleSignOnTokenRetainingScope = { + pairingRepository.invalidateSingleSignOnToken( + profileId + ) + }, + saveDecryptedAccessToken = { pairingRepository.saveDecryptedAccessToken(profileId, it) } + ) + } + } + + private suspend fun loadAccessToken( + refresh: Boolean = false, + profileId: ProfileIdentifier, + scope: IdpScope, + singleSignOnTokenScope: suspend () -> IdpData.SingleSignOnTokenScope?, + decryptedAccessToken: suspend () -> String?, + invalidateDecryptedAccessToken: suspend () -> Unit, + invalidateSingleSignOnTokenRetainingScope: suspend () -> Unit, + saveDecryptedAccessToken: suspend (decryptedAccessToken: String) -> Unit + ): String { + val ssoTokenScope = singleSignOnTokenScope() + + Napier.d { + """Loading access token with: + |refresh: $refresh + |profileId: $profileId + |scope: $scope + """.trimMargin() + } + + return if (ssoTokenScope != null) { + if (ssoTokenScope.token?.token == null) { + invalidateDecryptedAccessToken() + throw RefreshFlowException( + true, + ssoTokenScope, + "SSO token not set for $profileId!" + ) + } + + val accToken = decryptedAccessToken() + + if (refresh || accToken == null) { + invalidateDecryptedAccessToken() + + val actualToken = ssoTokenScope.token!!.token + + val initialData = try { + basicUseCase.initializeConfigurationAndKeys() + } catch (e: Exception) { + throw IDPConfigException(e) + } + try { + val refreshData = basicUseCase.refreshAccessTokenWithSsoFlow( + initialData, + scope = scope, + ssoToken = actualToken, + redirectUri = if (ssoTokenScope is IdpData.ExternalAuthenticationToken) { + EXT_AUTH_REDIRECT_URI + } else { + REDIRECT_URI + } + ) + refreshData.accessToken + } catch (e: Exception) { + Napier.e("Couldn't refresh access token", e) + (e as? ApiCallException)?.also { + when (it.response.code()) { + // 400 returned by redirect call if sso token is not valid anymore + 400, 401, 403 -> { + invalidateSingleSignOnTokenRetainingScope() + throw RefreshFlowException(true, ssoTokenScope, e) + } + } + } + throw RefreshFlowException(false, null, e) + } + } else { + accToken + } + .also { + saveDecryptedAccessToken(it) + } + } else { + invalidateDecryptedAccessToken() + throw RefreshFlowException( + true, + ssoTokenScope, + "SSO token not set for $profileId!" + ) + } + } + + /** + * Initial flow fetching the sso & access token requiring the health card to sign the challenge. + */ + @Requirement( + "A_20600#1", + "A_20601", + "A_20601-01", + "A_21598#2", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Authenticate to the IDP using the health card certificate." + ) + override suspend fun authenticationFlowWithHealthCard( + profileId: ProfileIdentifier, + scope: IdpScope, + cardAccessNumber: String, + healthCardCertificate: suspend () -> ByteArray, + sign: suspend (hash: ByteArray) -> ByteArray + ) { + lock.withLock { + authenticationFlowWithHealthCard( + cardAccessNumber = cardAccessNumber, + scope = scope, + healthCardCertificate = healthCardCertificate, + sign = sign + ) { _, _, basicData, ssoToken -> + when (scope) { + IdpScope.Default -> { + profilesRepository.saveInsuranceInformation( + profileId, + basicData.idTokenInsurantName, + basicData.idTokenInsuranceIdentifier, + basicData.idTokenInsuranceName + ) + repository.saveSingleSignOnToken(profileId, ssoToken) + repository.saveDecryptedAccessToken(profileId, basicData.accessToken) + } + + IdpScope.BiometricPairing -> { + pairingRepository.saveSingleSignOnToken( + profileId, + IdpData.SingleSignOnToken(basicData.ssoToken) + ) + } + } + } + } + } + + private suspend fun authenticationFlowWithHealthCard( + cardAccessNumber: String, + scope: IdpScope, + healthCardCertificate: suspend () -> ByteArray, + sign: suspend (hash: ByteArray) -> ByteArray, + finally: suspend ( + initialData: IdpInitialData, + healthCardCertificate: ByteArray, + basicData: IdpAuthFlowResult, + ssoToken: IdpData.DefaultToken + ) -> R + ): R { + val initialData = basicUseCase.initializeConfigurationAndKeys() + val challengeData = + basicUseCase.challengeFlow(initialData, scope = scope, redirectUri = REDIRECT_URI) + val cert = healthCardCertificate() + val basicData = basicUseCase.basicAuthFlow( + initialData = initialData, + challengeData = challengeData, + healthCardCertificate = cert, + sign = sign + ) + val ssoToken = IdpData.DefaultToken( + token = IdpData.SingleSignOnToken(basicData.ssoToken), + healthCardCertificate = cert, + cardAccessNumber = cardAccessNumber + ) + + return finally( + initialData, + cert, + basicData, + ssoToken + ) + } + + /** + * Get all the information for the correct endpoints from the discovery document and request + * the external Health Insurance Companies which are capable of authenticate you with their app + */ + @Requirement( + "A_22296-01#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Load list of external authenticators for Fast Track." + ) + override suspend fun loadExternAuthenticatorIDs(): List { + val initialData = basicUseCase.initializeConfigurationAndKeys() + return repository.fetchExternalAuthorizationIDList( + url = initialData.config.externalAuthorizationIDsEndpoint ?: error("Fasttrack is not available"), + idpPukSigKey = initialData.config.certificate.extractECPublicKey() + ).sortedBy { + it.name.lowercase() + } + } + + /** + * With chosen Health Insurance Company, request IDP for Authentication information, + * sent as a redirect which is supposed to be fired as an Intent + * @param externalAuthorizationId identifier of the health insurance company + */ + override suspend fun getUniversalLinkForExternalAuthorization( + profileId: ProfileIdentifier, + authenticatorId: String, + authenticatorName: String, + scope: IdpScope + ): URI { + val initialData = basicUseCase.initializeConfigurationAndKeys() + + val redirectUri = repository.getAuthorizationRedirect( + url = initialData.config.thirdPartyAuthorizationEndpoint ?: error("Fasttrack is not available"), + state = initialData.state, + codeChallenge = initialData.codeChallenge, + nonce = initialData.nonce, + kkAppId = authenticatorId, + scope = scope + ) + + val parsedUri = URI(redirectUri) + + preferences.externalAuthenticationPreferences = + ExternalAuthenticationPreferences( + extAuthCodeChallenge = initialData.codeChallenge, + extAuthCodeVerifier = initialData.codeVerifier, + extAuthState = IdpService.extractQueryParameter(parsedUri, "state"), + extAuthNonce = initialData.nonce.nonce, + extAuthId = authenticatorId, + extAuthScope = scope.name, + extAuthName = authenticatorName, + extAuthProfile = profileId + ) + + return parsedUri + } + + /** + * The scope is determined by the previously saved value within the shared prefs as `EXT_AUTH_SCOPE`. + */ + @Requirement( + "A_20527#2", + "A_20600#2", + "A_20601", + "A_20601-01", + "A_22301", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "External authentication (fast track)" + ) + @Requirement( + "O.Plat_10#1", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Follow redirect" + ) + override suspend fun authenticateWithExternalAppAuthorization( + uri: URI + ) { + lock.withLock { + val scope = preferences.externalAuthenticationPreferences.extAuthScope!! + val profileId = preferences.externalAuthenticationPreferences.extAuthProfile!! + + val externalAuthorizationData = ExternalAuthorizationData(uri) + + require(externalAuthorizationData.state == preferences.externalAuthenticationPreferences.extAuthState) + + val initialData = basicUseCase.initializeConfigurationAndKeys() + val redirectStringResult = repository.postExternAppAuthorizationData( + url = initialData.config.thirdPartyAuthorizationEndpoint ?: error("Fasttrack is not available"), + externalAuthorizationData = externalAuthorizationData + ) + val redirect = URI(redirectStringResult.getOrThrow()) + + val redirectCodeJwe = IdpService.extractQueryParameter(redirect, "code") + val redirectSsoToken = IdpService.extractQueryParameter(redirect, "ssotoken") + + val idpTokenResult = basicUseCase.postCodeAndDecryptAccessToken( + url = initialData.config.tokenEndpoint, + nonce = IdpNonce(preferences.externalAuthenticationPreferences.extAuthNonce!!), + codeVerifier = preferences.externalAuthenticationPreferences.extAuthCodeVerifier!!, + code = redirectCodeJwe, + pukEncKey = initialData.pukEncKey, + pukSigKey = initialData.pukSigKey, + redirectUri = EXT_AUTH_REDIRECT_URI + ) + + val authId = preferences.externalAuthenticationPreferences.extAuthId!! + val authName = preferences.externalAuthenticationPreferences.extAuthName!! + + preferences.clear() + + when (scope) { + IdpScope.Default.name -> { + val idTokenJson = Json.parseToJsonElement(idpTokenResult.idTokenPayload) + + val idTokenInsuranceIdentifier = idTokenJson.jsonObject["idNummer"]?.jsonPrimitive?.content ?: "" + val idTokenInsuranceName = idTokenJson.jsonObject["organizationName"]?.jsonPrimitive?.content ?: "" + val idTokenInsurantName = idTokenJson.jsonObject["given_name"]?.jsonPrimitive?.content + ?.let { + "$it ${idTokenJson.jsonObject["family_name"]?.jsonPrimitive?.content}" + } ?: "" + + profilesRepository.saveInsuranceInformation( + profileId = profileId, + insurantName = idTokenInsurantName, + insuranceIdentifier = idTokenInsuranceIdentifier, + insuranceName = idTokenInsuranceName + ) + + repository.saveSingleSignOnToken( + profileId, + IdpData.ExternalAuthenticationToken( + token = IdpData.SingleSignOnToken(redirectSsoToken), + authenticatorId = authId, + authenticatorName = authName + ) + ) + repository.saveDecryptedAccessToken(profileId, idpTokenResult.decryptedAccessToken) + } + + IdpScope.BiometricPairing.name -> { + pairingRepository.saveSingleSignOnToken( + profileId, + IdpData.SingleSignOnToken(redirectSsoToken) + ) + } + } + } + } + + /** + * Pairing flow fetching the sso & access token requiring the health card and generated key material. + */ + override suspend fun alternatePairingFlowWithSecureElement( + profileId: ProfileIdentifier, + cardAccessNumber: String, + publicKeyOfSecureElementEntry: PublicKey, + aliasOfSecureElementEntry: ByteArray, + healthCardCertificate: suspend () -> ByteArray, + signWithHealthCard: suspend (hash: ByteArray) -> ByteArray + ) = lock.withLock { + val initialData = basicUseCase.initializeConfigurationAndKeys() + val challengeData = + basicUseCase.challengeFlow( + initialData, + scope = IdpScope.BiometricPairing, + redirectUri = REDIRECT_URI + ) + val healthCardCert = healthCardCertificate() + val basicData = basicUseCase.basicAuthFlow( + initialData = initialData, + challengeData = challengeData, + healthCardCertificate = healthCardCert, + sign = signWithHealthCard + ) + + altAuthUseCase.registerDeviceWithHealthCard( + initialData = initialData, + accessToken = basicData.accessToken, + healthCardCertificate = healthCardCert, + publicKeyOfSecureElementEntry = publicKeyOfSecureElementEntry, + aliasOfSecureElementEntry = aliasOfSecureElementEntry, + signWithHealthCard = signWithHealthCard + ) + profilesRepository.saveInsuranceInformation( + profileId, + basicData.idTokenInsurantName, + basicData.idTokenInsuranceIdentifier, + basicData.idTokenInsuranceName + ) + // set pairing scope + repository.saveSingleSignOnToken( + profileId, + IdpData.AlternateAuthenticationWithoutToken( + cardAccessNumber = cardAccessNumber, + aliasOfSecureElementEntry = aliasOfSecureElementEntry, + healthCardCertificate = healthCardCert + ) + ) + } + + /** + * Actual authentication with secure element key material. Just like the [authenticationFlowWithHealthCard] it + * sets the sso & access token within the repository. + */ + @Requirement( + "A_21598#1", + sourceSpecification = "gemSpec_IDP_Frontend", + rationale = "Authentication flow with health card and secure element." + ) + override suspend fun alternateAuthenticationFlowWithSecureElement( + profileId: ProfileIdentifier, + scope: IdpScope + ) { + lock.withLock { + alternateAuthenticationFlowWithSecureElement( + profileId = profileId, + scope = IdpScope.Default + ) { _, authTokenScope, authData -> + when (scope) { + IdpScope.Default -> { + profilesRepository.saveInsuranceInformation( + profileId, + authData.idTokenInsurantName, + authData.idTokenInsuranceIdentifier, + authData.idTokenInsuranceName + ) + repository.saveSingleSignOnToken( + profileId, + IdpData.AlternateAuthenticationToken( + IdpData.SingleSignOnToken(authData.ssoToken), + cardAccessNumber = authTokenScope.cardAccessNumber, + aliasOfSecureElementEntry = authTokenScope.aliasOfSecureElementEntry, + healthCardCertificate = authTokenScope.healthCardCertificate.encoded + ) + ) + repository.saveDecryptedAccessToken(profileId, authData.accessToken) + } + + IdpScope.BiometricPairing -> { + pairingRepository.saveSingleSignOnToken( + profileId, + IdpData.SingleSignOnToken(authData.ssoToken) + ) + } + } + } + } + } + + private suspend fun alternateAuthenticationFlowWithSecureElement( + profileId: ProfileIdentifier, + scope: IdpScope, + finally: suspend ( + initialData: IdpInitialData, + authTokenScope: IdpData.TokenWithKeyStoreAliasScope, + authData: IdpAuthFlowResult + ) -> R + ): R { + val ssoTokenScope = requireNotNull(repository.authenticationData(profileId).first().singleSignOnTokenScope) + + val authTokenScope = + requireNotNull(ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope) { "Wrong authentication scope!" } + + val healthCardCertificate = authTokenScope.healthCardCertificate + val aliasOfSecureElementEntry = authTokenScope.aliasOfSecureElementEntry + + lateinit var privateKeyOfSecureElementEntry: PrivateKey + lateinit var signatureObjectOfSecureElementEntry: Signature + @Requirement( + "O.Cryp_1#2", + "O.Cryp_4#2", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Signature via ecdh ephemeral-static (one time usage)" + ) + @Requirement( + "O.Cryp_6", + sourceSpecification = "BSI-eRp-ePA", + rationale = "Persisted cryptographic keys are created within the devices key store. " + + "Temporal keys are discarded as soon as usage is no longer needed." + ) + @Requirement( + "O.Cryp_7", + sourceSpecification = "BSI-eRp-ePA", + rationale = "As Brainpool256R1 is not available within key store but enforced by BSI where possible, " + + "we use secure enclave encryption only for biometric authentication. " + + "Everywhere else, cryptographic operations are ephemeral or use the eGK " + + "as a secure execution environment." + ) + try { + privateKeyOfSecureElementEntry = ( + cryptoProvider.keyStoreInstance() + .apply { load(null) } + .getEntry( + Base64.toBase64String(aliasOfSecureElementEntry), + null + ) as KeyStore.PrivateKeyEntry + ).privateKey + signatureObjectOfSecureElementEntry = cryptoProvider.signatureInstance() + } catch (e: Exception) { + // the system might have removed the key during biometric re-enrollment + // therefore there's no choice but to delete everything + repository.invalidate(profileId) + throw AltAuthenticationCryptoException(e) + } + + val initialData = basicUseCase.initializeConfigurationAndKeys() + val challengeData = basicUseCase.challengeFlow(initialData, scope = scope, redirectUri = REDIRECT_URI) + + val authData = altAuthUseCase.authenticateWithSecureElement( + initialData = initialData, + challenge = challengeData.challenge, + healthCardCertificate = healthCardCertificate.encoded, + authenticationMethod = IdpAlternateAuthenticationUseCase.AuthenticationMethod.Strong, + aliasOfSecureElementEntry = aliasOfSecureElementEntry, + privateKeyOfSecureElementEntry = privateKeyOfSecureElementEntry, + signatureObjectOfSecureElementEntry = signatureObjectOfSecureElementEntry + ) + + return finally( + initialData, + authTokenScope, + authData + ) + } + + /** + * Returns the paired devices associated with the [profileId]s sso token scope. + * + * @param authenticateWithSecureElement will be called if an alternate authentication is required. + * @param authenticateWithHealthCard will be called if a health card authentication is required + * which needs to sign [hash]. + */ + override suspend fun getPairedDevices(profileId: ProfileIdentifier): + Result>> = + redoOnce { + val accessToken = loadAccessToken( + refresh = it, + profileId = profileId, + scope = IdpScope.BiometricPairing + ) + + altAuthUseCase.getPairedDevices( + initialData = basicUseCase.initializeConfigurationAndKeys(), + accessToken = accessToken + ) + } + + /** + * Deletes the device identified by [deviceAlias]. + */ + override suspend fun deletePairedDevice(profileId: ProfileIdentifier, deviceAlias: String) = + redoOnce { + val accessToken = loadAccessToken( + refresh = it, + profileId = profileId, + scope = IdpScope.BiometricPairing + ) + + altAuthUseCase.deletePairedDevice( + initialData = basicUseCase.initializeConfigurationAndKeys(), + accessToken = accessToken, + deviceAlias = deviceAlias + ) + } + + private suspend fun redoOnce( + block: suspend (retry: Boolean) -> R + ) = + runCatching { + block(false) + }.recoverCatching { e -> + val isRetryable = (e as? ApiCallException)?.let { + it.response.code() == HttpURLConnection.HTTP_FORBIDDEN || + it.response.code() == HttpURLConnection.HTTP_UNAUTHORIZED + } ?: false + if (isRetryable) { + block(true) + } else { + throw e + } + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt new file mode 100644 index 00000000..9d1b3be6 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IDPConfigException.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.idp.usecase + +import java.io.IOException + +class IDPConfigException(cause: Throwable) : IOException(cause) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt index 1b31c962..97d07d79 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpUseCase.kt @@ -15,682 +15,65 @@ * limitations under the Licence. * */ +@file:Suppress("LongParameterList") package de.gematik.ti.erp.app.idp.usecase -import de.gematik.ti.erp.app.Requirement -import de.gematik.ti.erp.app.api.ApiCallException -import de.gematik.ti.erp.app.idp.api.EXT_AUTH_REDIRECT_URI -import de.gematik.ti.erp.app.idp.api.IdpService -import de.gematik.ti.erp.app.idp.api.REDIRECT_URI import de.gematik.ti.erp.app.idp.api.models.AuthenticationId -import de.gematik.ti.erp.app.idp.api.models.ExternalAuthorizationData -import de.gematik.ti.erp.app.idp.api.models.IdpAuthFlowResult -import de.gematik.ti.erp.app.idp.api.models.IdpInitialData -import de.gematik.ti.erp.app.idp.api.models.IdpNonce import de.gematik.ti.erp.app.idp.api.models.IdpScope import de.gematik.ti.erp.app.idp.api.models.PairingData import de.gematik.ti.erp.app.idp.api.models.PairingResponseEntry -import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository -import de.gematik.ti.erp.app.idp.repository.IdpRepository import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository -import de.gematik.ti.erp.app.vau.extractECPublicKey -import java.io.IOException import java.net.URI -import java.security.KeyStore -import java.security.PrivateKey import java.security.PublicKey -import java.security.Signature -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.bouncycastle.util.encoders.Base64 -import io.github.aakira.napier.Napier -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import java.net.HttpURLConnection -/** - * Exception thrown by [IdpUseCase.loadAccessToken]. - */ -class RefreshFlowException : IOException { - /** - * Is true if the sso token is not valid anymore and the user is required to authenticate again. - */ - val userActionRequired: Boolean - val ssoToken: IdpData.SingleSignOnTokenScope? - - constructor( - userActionRequired: Boolean, - ssoToken: IdpData.SingleSignOnTokenScope?, - cause: Throwable - ) : super(cause) { - this.userActionRequired = userActionRequired - this.ssoToken = ssoToken - } - - constructor( - userActionRequired: Boolean, - ssoToken: IdpData.SingleSignOnTokenScope?, - message: String - ) : super(message) { - this.userActionRequired = userActionRequired - this.ssoToken = ssoToken - } -} - -class IDPConfigException(cause: Throwable) : IOException(cause) - -class AltAuthenticationCryptoException(cause: Throwable) : IllegalStateException(cause) +private typealias ProfileId = ProfileIdentifier -class IdpUseCase( - private val repository: IdpRepository, - private val pairingRepository: IdpPairingRepository, - private val altAuthUseCase: IdpAlternateAuthenticationUseCase, - private val profilesRepository: ProfilesRepository, - private val basicUseCase: IdpBasicUseCase, - private val preferences: IdpPreferenceProvider, - private val cryptoProvider: IdpCryptoProvider -) { - private val lock = Mutex() +interface IdpUseCase { - /** - * If no bearer token is set or [refresh] is true, this will trigger [IdpBasicUseCase.refreshAccessTokenWithSsoFlow]. - */ - @Requirement( - "A_20283-01#1", - "A_21326", - "A_21327", - sourceSpecification = "gemSpec_eRp_FdV", - rationale = "Load and decrypt access token." - ) suspend fun loadAccessToken( - refresh: Boolean = false, profileId: ProfileIdentifier, + refresh: Boolean, scope: IdpScope = IdpScope.Default - ): String = lock.withLock { - when (scope) { - IdpScope.Default -> - loadAccessToken( - refresh = refresh, - profileId = profileId, - scope = IdpScope.Default, - singleSignOnTokenScope = { - repository.authenticationData(profileId).first().singleSignOnTokenScope - }, - decryptedAccessToken = { repository.decryptedAccessToken(profileId).first() }, - invalidateDecryptedAccessToken = { repository.invalidateDecryptedAccessToken(profileId) }, - invalidateSingleSignOnTokenRetainingScope = { - repository.invalidateSingleSignOnTokenRetainingScope( - profileId - ) - }, - saveDecryptedAccessToken = { repository.saveDecryptedAccessToken(profileId, it) } - ) - IdpScope.BiometricPairing -> - loadAccessToken( - refresh = refresh, - profileId = profileId, - scope = IdpScope.BiometricPairing, - singleSignOnTokenScope = { pairingRepository.singleSignOnTokenScope(profileId).first() }, - decryptedAccessToken = { pairingRepository.decryptedAccessToken(profileId).first() }, - invalidateDecryptedAccessToken = { pairingRepository.invalidateDecryptedAccessToken(profileId) }, - invalidateSingleSignOnTokenRetainingScope = { - pairingRepository.invalidateSingleSignOnToken( - profileId - ) - }, - saveDecryptedAccessToken = { pairingRepository.saveDecryptedAccessToken(profileId, it) } - ) - } - } - - private suspend fun loadAccessToken( - refresh: Boolean = false, - profileId: ProfileIdentifier, - scope: IdpScope, - singleSignOnTokenScope: suspend () -> IdpData.SingleSignOnTokenScope?, - decryptedAccessToken: suspend () -> String?, - invalidateDecryptedAccessToken: suspend () -> Unit, - invalidateSingleSignOnTokenRetainingScope: suspend () -> Unit, - saveDecryptedAccessToken: suspend (decryptedAccessToken: String) -> Unit - ): String { - val ssoTokenScope = singleSignOnTokenScope() - - Napier.d { - """Loading access token with: - |refresh: $refresh - |profileId: $profileId - |scope: $scope - """.trimMargin() - } - - return if (ssoTokenScope != null) { - if (ssoTokenScope.token?.token == null) { - invalidateDecryptedAccessToken() - throw RefreshFlowException( - true, - ssoTokenScope, - "SSO token not set for $profileId!" - ) - } - - val accToken = decryptedAccessToken() - - if (refresh || accToken == null) { - invalidateDecryptedAccessToken() - - val actualToken = ssoTokenScope.token!!.token - - val initialData = try { - basicUseCase.initializeConfigurationAndKeys() - } catch (e: Exception) { - throw IDPConfigException(e) - } - try { - val refreshData = basicUseCase.refreshAccessTokenWithSsoFlow( - initialData, - scope = scope, - ssoToken = actualToken, - redirectUri = if (ssoTokenScope is IdpData.ExternalAuthenticationToken) { - EXT_AUTH_REDIRECT_URI - } else { - REDIRECT_URI - } - ) - refreshData.accessToken - } catch (e: Exception) { - Napier.e("Couldn't refresh access token", e) - (e as? ApiCallException)?.also { - when (it.response.code()) { - // 400 returned by redirect call if sso token is not valid anymore - 400, 401, 403 -> { - invalidateSingleSignOnTokenRetainingScope() - throw RefreshFlowException(true, ssoTokenScope, e) - } - } - } - throw RefreshFlowException(false, null, e) - } - } else { - accToken - } - .also { - saveDecryptedAccessToken(it) - } - } else { - invalidateDecryptedAccessToken() - throw RefreshFlowException( - true, - ssoTokenScope, - "SSO token not set for $profileId!" - ) - } - } + ): String - /** - * Initial flow fetching the sso & access token requiring the health card to sign the challenge. - */ - @Requirement( - "A_20600#1", - "A_20601", - "A_20601-01", - "A_21598#2", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Authenticate to the IDP using the health card certificate." - ) suspend fun authenticationFlowWithHealthCard( - profileId: ProfileIdentifier, + profileId: ProfileId, scope: IdpScope = IdpScope.Default, cardAccessNumber: String, healthCardCertificate: suspend () -> ByteArray, sign: suspend (hash: ByteArray) -> ByteArray - ) { - lock.withLock { - authenticationFlowWithHealthCard( - cardAccessNumber = cardAccessNumber, - scope = scope, - healthCardCertificate = healthCardCertificate, - sign = sign - ) { _, _, basicData, ssoToken -> - when (scope) { - IdpScope.Default -> { - profilesRepository.saveInsuranceInformation( - profileId, - basicData.idTokenInsurantName, - basicData.idTokenInsuranceIdentifier, - basicData.idTokenInsuranceName - ) - repository.saveSingleSignOnToken(profileId, ssoToken) - repository.saveDecryptedAccessToken(profileId, basicData.accessToken) - } - IdpScope.BiometricPairing -> { - pairingRepository.saveSingleSignOnToken( - profileId, - IdpData.SingleSignOnToken(basicData.ssoToken) - ) - } - } - } - } - } - - private suspend fun authenticationFlowWithHealthCard( - cardAccessNumber: String, - scope: IdpScope, - healthCardCertificate: suspend () -> ByteArray, - sign: suspend (hash: ByteArray) -> ByteArray, - finally: suspend ( - initialData: IdpInitialData, - healthCardCertificate: ByteArray, - basicData: IdpAuthFlowResult, - ssoToken: IdpData.DefaultToken - ) -> R - ): R { - val initialData = basicUseCase.initializeConfigurationAndKeys() - val challengeData = - basicUseCase.challengeFlow(initialData, scope = scope, redirectUri = REDIRECT_URI) - val cert = healthCardCertificate() - val basicData = basicUseCase.basicAuthFlow( - initialData = initialData, - challengeData = challengeData, - healthCardCertificate = cert, - sign = sign - ) - val ssoToken = IdpData.DefaultToken( - token = IdpData.SingleSignOnToken(basicData.ssoToken), - healthCardCertificate = cert, - cardAccessNumber = cardAccessNumber - ) - - return finally( - initialData, - cert, - basicData, - ssoToken - ) - } - - /** - * Get all the information for the correct endpoints from the discovery document and request - * the external Health Insurance Companies which are capable of authenticate you with their app - */ - @Requirement( - "A_22296-01#1", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Load list of external authenticators for Fast Track." ) - suspend fun loadExternAuthenticatorIDs(): List { - val initialData = basicUseCase.initializeConfigurationAndKeys() - return repository.fetchExternalAuthorizationIDList( - url = initialData.config.externalAuthorizationIDsEndpoint ?: error("Fasttrack is not available"), - idpPukSigKey = initialData.config.certificate.extractECPublicKey() - ).sortedBy { - it.name.lowercase() - } - } - /** - * With chosen Health Insurance Company, request IDP for Authentication information, - * sent as a redirect which is supposed to be fired as an Intent - * @param externalAuthorizationId identifier of the health insurance company - */ + suspend fun loadExternAuthenticatorIDs(): List + suspend fun getUniversalLinkForExternalAuthorization( - profileId: ProfileIdentifier, + profileId: ProfileId, authenticatorId: String, authenticatorName: String, scope: IdpScope = IdpScope.Default - ): URI { - val initialData = basicUseCase.initializeConfigurationAndKeys() + ): URI - val redirectUri = repository.getAuthorizationRedirect( - url = initialData.config.thirdPartyAuthorizationEndpoint ?: error("Fasttrack is not available"), - state = initialData.state, - codeChallenge = initialData.codeChallenge, - nonce = initialData.nonce, - kkAppId = authenticatorId, - scope = scope - ) - - val parsedUri = URI(redirectUri) - - preferences.externalAuthenticationPreferences = - ExternalAuthenticationPreferences( - extAuthCodeChallenge = initialData.codeChallenge, - extAuthCodeVerifier = initialData.codeVerifier, - extAuthState = IdpService.extractQueryParameter(parsedUri, "state"), - extAuthNonce = initialData.nonce.nonce, - extAuthId = authenticatorId, - extAuthScope = scope.name, - extAuthName = authenticatorName, - extAuthProfile = profileId - ) - - return parsedUri - } - - /** - * The scope is determined by the previously saved value within the shared prefs as `EXT_AUTH_SCOPE`. - */ - @Requirement( - "A_20527#2", - "A_20600#2", - "A_20601", - "A_20601-01", - "A_22301", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "External authentication (fast track)" - ) - @Requirement( - "O.Plat_10#1", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Follow redirect" - ) - suspend fun authenticateWithExternalAppAuthorization( - uri: URI - ) { - lock.withLock { - val scope = preferences.externalAuthenticationPreferences.extAuthScope!! - val profileId = preferences.externalAuthenticationPreferences.extAuthProfile!! - - val externalAuthorizationData = ExternalAuthorizationData(uri) - - require(externalAuthorizationData.state == preferences.externalAuthenticationPreferences.extAuthState) - - val initialData = basicUseCase.initializeConfigurationAndKeys() - val redirectStringResult = repository.postExternAppAuthorizationData( - url = initialData.config.thirdPartyAuthorizationEndpoint ?: error("Fasttrack is not available"), - externalAuthorizationData = externalAuthorizationData - ) - val redirect = URI(redirectStringResult.getOrThrow()) - - val redirectCodeJwe = IdpService.extractQueryParameter(redirect, "code") - val redirectSsoToken = IdpService.extractQueryParameter(redirect, "ssotoken") - - val idpTokenResult = basicUseCase.postCodeAndDecryptAccessToken( - url = initialData.config.tokenEndpoint, - nonce = IdpNonce(preferences.externalAuthenticationPreferences.extAuthNonce!!), - codeVerifier = preferences.externalAuthenticationPreferences.extAuthCodeVerifier!!, - code = redirectCodeJwe, - pukEncKey = initialData.pukEncKey, - pukSigKey = initialData.pukSigKey, - redirectUri = EXT_AUTH_REDIRECT_URI - ) - - val authId = preferences.externalAuthenticationPreferences.extAuthId!! - val authName = preferences.externalAuthenticationPreferences.extAuthName!! - - preferences.clear() - - when (scope) { - IdpScope.Default.name -> { - val idTokenJson = Json.parseToJsonElement(idpTokenResult.idTokenPayload) - - val idTokenInsuranceIdentifier = idTokenJson.jsonObject["idNummer"]?.jsonPrimitive?.content ?: "" - val idTokenInsuranceName = idTokenJson.jsonObject["organizationName"]?.jsonPrimitive?.content ?: "" - val idTokenInsurantName = idTokenJson.jsonObject["given_name"]?.jsonPrimitive?.content - ?.let { - "$it ${idTokenJson.jsonObject["family_name"]?.jsonPrimitive?.content}" - } ?: "" - - profilesRepository.saveInsuranceInformation( - profileId = profileId, - insurantName = idTokenInsurantName, - insuranceIdentifier = idTokenInsuranceIdentifier, - insuranceName = idTokenInsuranceName - ) - - repository.saveSingleSignOnToken( - profileId, - IdpData.ExternalAuthenticationToken( - token = IdpData.SingleSignOnToken(redirectSsoToken), - authenticatorId = authId, - authenticatorName = authName - ) - ) - repository.saveDecryptedAccessToken(profileId, idpTokenResult.decryptedAccessToken) - } - IdpScope.BiometricPairing.name -> { - pairingRepository.saveSingleSignOnToken( - profileId, - IdpData.SingleSignOnToken(redirectSsoToken) - ) - } - } - } - } + suspend fun authenticateWithExternalAppAuthorization(uri: URI) /** * Pairing flow fetching the sso & access token requiring the health card and generated key material. */ suspend fun alternatePairingFlowWithSecureElement( - profileId: ProfileIdentifier, + profileId: ProfileId, cardAccessNumber: String, publicKeyOfSecureElementEntry: PublicKey, aliasOfSecureElementEntry: ByteArray, healthCardCertificate: suspend () -> ByteArray, signWithHealthCard: suspend (hash: ByteArray) -> ByteArray - ) = lock.withLock { - val initialData = basicUseCase.initializeConfigurationAndKeys() - val challengeData = - basicUseCase.challengeFlow( - initialData, - scope = IdpScope.BiometricPairing, - redirectUri = REDIRECT_URI - ) - val healthCardCert = healthCardCertificate() - val basicData = basicUseCase.basicAuthFlow( - initialData = initialData, - challengeData = challengeData, - healthCardCertificate = healthCardCert, - sign = signWithHealthCard - ) - - altAuthUseCase.registerDeviceWithHealthCard( - initialData = initialData, - accessToken = basicData.accessToken, - healthCardCertificate = healthCardCert, - publicKeyOfSecureElementEntry = publicKeyOfSecureElementEntry, - aliasOfSecureElementEntry = aliasOfSecureElementEntry, - signWithHealthCard = signWithHealthCard - ) - profilesRepository.saveInsuranceInformation( - profileId, - basicData.idTokenInsurantName, - basicData.idTokenInsuranceIdentifier, - basicData.idTokenInsuranceName - ) - // set pairing scope - repository.saveSingleSignOnToken( - profileId, - IdpData.AlternateAuthenticationWithoutToken( - cardAccessNumber = cardAccessNumber, - aliasOfSecureElementEntry = aliasOfSecureElementEntry, - healthCardCertificate = healthCardCert - ) - ) - } + ): Unit - /** - * Actual authentication with secure element key material. Just like the [authenticationFlowWithHealthCard] it - * sets the sso & access token within the repository. - */ - @Requirement( - "A_21598#1", - sourceSpecification = "gemSpec_IDP_Frontend", - rationale = "Authentication flow with health card and secure element." - ) suspend fun alternateAuthenticationFlowWithSecureElement( - profileId: ProfileIdentifier, + profileId: ProfileId, scope: IdpScope = IdpScope.Default - ) { - lock.withLock { - alternateAuthenticationFlowWithSecureElement( - profileId = profileId, - scope = IdpScope.Default - ) { _, authTokenScope, authData -> - when (scope) { - IdpScope.Default -> { - profilesRepository.saveInsuranceInformation( - profileId, - authData.idTokenInsurantName, - authData.idTokenInsuranceIdentifier, - authData.idTokenInsuranceName - ) - repository.saveSingleSignOnToken( - profileId, - IdpData.AlternateAuthenticationToken( - IdpData.SingleSignOnToken(authData.ssoToken), - cardAccessNumber = authTokenScope.cardAccessNumber, - aliasOfSecureElementEntry = authTokenScope.aliasOfSecureElementEntry, - healthCardCertificate = authTokenScope.healthCardCertificate.encoded - ) - ) - repository.saveDecryptedAccessToken(profileId, authData.accessToken) - } - IdpScope.BiometricPairing -> { - pairingRepository.saveSingleSignOnToken( - profileId, - IdpData.SingleSignOnToken(authData.ssoToken) - ) - } - } - } - } - } - - private suspend fun alternateAuthenticationFlowWithSecureElement( - profileId: ProfileIdentifier, - scope: IdpScope, - finally: suspend ( - initialData: IdpInitialData, - authTokenScope: IdpData.TokenWithKeyStoreAliasScope, - authData: IdpAuthFlowResult - ) -> R - ): R { - val ssoTokenScope = requireNotNull(repository.authenticationData(profileId).first().singleSignOnTokenScope) - - val authTokenScope = - requireNotNull(ssoTokenScope as? IdpData.TokenWithKeyStoreAliasScope) { "Wrong authentication scope!" } - - val healthCardCertificate = authTokenScope.healthCardCertificate - val aliasOfSecureElementEntry = authTokenScope.aliasOfSecureElementEntry - - lateinit var privateKeyOfSecureElementEntry: PrivateKey - lateinit var signatureObjectOfSecureElementEntry: Signature - @Requirement( - "O.Cryp_1#2", - "O.Cryp_4#2", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Signature via ecdh ephemeral-static (one time usage)" - ) - @Requirement( - "O.Cryp_6", - sourceSpecification = "BSI-eRp-ePA", - rationale = "Persisted cryptographic keys are created within the devices key store. " + - "Temporal keys are discarded as soon as usage is no longer needed." - ) - @Requirement( - "O.Cryp_7", - sourceSpecification = "BSI-eRp-ePA", - rationale = "As Brainpool256R1 is not available within key store but enforced by BSI where possible, " + - "we use secure enclave encryption only for biometric authentication. " + - "Everywhere else, cryptographic operations are ephemeral or use the eGK " + - "as a secure execution environment." - ) - try { - privateKeyOfSecureElementEntry = ( - cryptoProvider.keyStoreInstance() - .apply { load(null) } - .getEntry( - Base64.toBase64String(aliasOfSecureElementEntry), - null - ) as KeyStore.PrivateKeyEntry - ).privateKey - signatureObjectOfSecureElementEntry = cryptoProvider.signatureInstance() - } catch (e: Exception) { - // the system might have removed the key during biometric re-enrollment - // therefore there's no choice but to delete everything - repository.invalidate(profileId) - throw AltAuthenticationCryptoException(e) - } - - val initialData = basicUseCase.initializeConfigurationAndKeys() - val challengeData = basicUseCase.challengeFlow(initialData, scope = scope, redirectUri = REDIRECT_URI) - - val authData = altAuthUseCase.authenticateWithSecureElement( - initialData = initialData, - challenge = challengeData.challenge, - healthCardCertificate = healthCardCertificate.encoded, - authenticationMethod = IdpAlternateAuthenticationUseCase.AuthenticationMethod.Strong, - aliasOfSecureElementEntry = aliasOfSecureElementEntry, - privateKeyOfSecureElementEntry = privateKeyOfSecureElementEntry, - signatureObjectOfSecureElementEntry = signatureObjectOfSecureElementEntry - ) - - return finally( - initialData, - authTokenScope, - authData - ) - } - - /** - * Returns the paired devices associated with the [profileId]s sso token scope. - * - * @param authenticateWithSecureElement will be called if an alternate authentication is required. - * @param authenticateWithHealthCard will be called if a health card authentication is required - * which needs to sign [hash]. - */ - suspend fun getPairedDevices(profileId: ProfileIdentifier): Result>> = - redoOnce { - val accessToken = loadAccessToken( - refresh = it, - profileId = profileId, - scope = IdpScope.BiometricPairing - ) - - altAuthUseCase.getPairedDevices( - initialData = basicUseCase.initializeConfigurationAndKeys(), - accessToken = accessToken - ) - } - - /** - * Deletes the device identified by [deviceAlias]. - */ - suspend fun deletePairedDevice(profileId: ProfileIdentifier, deviceAlias: String) = - redoOnce { - val accessToken = loadAccessToken( - refresh = it, - profileId = profileId, - scope = IdpScope.BiometricPairing - ) + ) - altAuthUseCase.deletePairedDevice( - initialData = basicUseCase.initializeConfigurationAndKeys(), - accessToken = accessToken, - deviceAlias = deviceAlias - ) - } + suspend fun getPairedDevices(profileId: ProfileId): Result>> - private suspend fun redoOnce( - block: suspend (retry: Boolean) -> R - ) = - runCatching { - block(false) - }.recoverCatching { e -> - val isRetryable = (e as? ApiCallException)?.let { - it.response.code() == HttpURLConnection.HTTP_FORBIDDEN || - it.response.code() == HttpURLConnection.HTTP_UNAUTHORIZED - } ?: false - if (isRetryable) { - block(true) - } else { - throw e - } - } + suspend fun deletePairedDevice(profileId: ProfileId, deviceAlias: String): Result } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt new file mode 100644 index 00000000..ad4dd0b1 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/idp/usecase/RefreshFlowException.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.idp.usecase + +import de.gematik.ti.erp.app.idp.model.IdpData +import java.io.IOException + +/** + * Exception thrown by [IdpUseCase.loadAccessToken]. + */ +class RefreshFlowException : IOException { + /** + * Is true if the sso token is not valid anymore and the user is required to authenticate again. + */ + val isUserAction: Boolean + val ssoToken: IdpData.SingleSignOnTokenScope? + + constructor( + userActionRequired: Boolean, + ssoToken: IdpData.SingleSignOnTokenScope?, + cause: Throwable + ) : super(cause) { + this.isUserAction = userActionRequired + this.ssoToken = ssoToken + } + + constructor( + userActionRequired: Boolean, + ssoToken: IdpData.SingleSignOnTokenScope?, + message: String + ) : super(message) { + this.isUserAction = userActionRequired + this.ssoToken = ssoToken + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt index ccf038d4..d6c36a99 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepository.kt @@ -48,10 +48,10 @@ class InvoiceRepository( } fun invoices(profileId: ProfileIdentifier) = - localDataSource.loadInvoices(profileId).flowOn(dispatchers.IO) + localDataSource.loadInvoices(profileId).flowOn(dispatchers.io) fun invoiceById(taskId: String) = - localDataSource.loadInvoiceById(taskId).flowOn(dispatchers.IO) + localDataSource.loadInvoiceById(taskId).flowOn(dispatchers.io) suspend fun saveInvoice(profileId: ProfileIdentifier, bundle: JsonElement) { localDataSource.saveInvoice(profileId, bundle) @@ -69,7 +69,7 @@ class InvoiceRepository( lastUpdated = timestamp, count = count ).mapCatching { fhirBundle -> - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val (total, taskIds) = extractTaskIdsFromChargeItemBundle(fhirBundle) supervisorScope { @@ -87,7 +87,7 @@ class InvoiceRepository( private suspend fun downloadInvoiceWithBundle( taskId: String, profileId: ProfileIdentifier - ) = withContext(dispatchers.IO) { + ) = withContext(dispatchers.io) { remoteDataSource.getChargeItemBundleById(profileId, taskId).mapCatching { bundle -> requireNotNull(localDataSource.saveInvoice(profileId, bundle)) } @@ -96,7 +96,7 @@ class InvoiceRepository( suspend fun deleteInvoiceById( taskId: String, profileId: ProfileIdentifier - ) = withContext(dispatchers.IO) { + ) = withContext(dispatchers.io) { val result = remoteDataSource.deleteChargeItemById(profileId, taskId) .onSuccess { localDataSource.deleteInvoiceById(taskId) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt index 22d830d0..90e9fa63 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/invoice/usecase/InvoiceUseCase.kt @@ -43,7 +43,7 @@ class InvoiceUseCase( private val dispatchers: DispatchProvider ) { fun invoicesFlow(profileId: ProfileIdentifier): Flow> = - invoiceRepository.invoices(profileId).flowOn(dispatchers.IO) + invoiceRepository.invoices(profileId).flowOn(dispatchers.io) fun invoices(profileId: ProfileIdentifier): Flow>> = invoicesFlow(profileId).map { invoices -> @@ -65,7 +65,7 @@ class InvoiceUseCase( val forProfileId: ProfileIdentifier ) - private val scope = CoroutineScope(dispatchers.IO) + private val scope = CoroutineScope(dispatchers.io) private val requestChannel = Channel(onUndeliveredElement = { it.resultChannel.close(CancellationException()) }) diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt similarity index 89% rename from android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt index 222e053f..a2a1a808 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationLocalDataSource.kt @@ -23,7 +23,8 @@ package de.gematik.ti.erp.app.orders.repository import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 import de.gematik.ti.erp.app.db.queryFirst import de.gematik.ti.erp.app.db.toInstant -import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile import de.gematik.ti.erp.app.prescription.repository.toCommunication import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import io.realm.kotlin.Realm @@ -40,11 +41,11 @@ class CommunicationLocalDataSource( fun loadDispReqCommunications( orderId: String - ): Flow> = + ): Flow> = realm.query( "orderId = $0 && _profile = $1", orderId, - SyncedTaskData.CommunicationProfile.ErxCommunicationDispReq.toEntityValue() + CommunicationProfile.ErxCommunicationDispReq.toEntityValue() ) .asFlow() .map { communication -> @@ -55,11 +56,11 @@ class CommunicationLocalDataSource( fun loadFirstDispReqCommunications( profileId: ProfileIdentifier - ): Flow> = + ): Flow> = realm.query( "parent.parent.id = $0 && _profile = $1", profileId, - SyncedTaskData.CommunicationProfile.ErxCommunicationDispReq.toEntityValue() + CommunicationProfile.ErxCommunicationDispReq.toEntityValue() ) .sort("sentOn", Sort.DESCENDING) .distinct("orderId") @@ -72,12 +73,12 @@ class CommunicationLocalDataSource( fun loadRepliedCommunications( taskIds: List - ): Flow> = + ): Flow> = realm.query( orQuerySubstring("parent.taskId", taskIds.size), *taskIds.toTypedArray() ) - .query("_profile = $0", SyncedTaskData.CommunicationProfile.ErxCommunicationReply.toEntityValue()) + .query("_profile = $0", CommunicationProfile.ErxCommunicationReply.toEntityValue()) .sort("sentOn", Sort.DESCENDING) .distinct("payload") .asFlow() @@ -123,7 +124,7 @@ class CommunicationLocalDataSource( realm.query( "orderId = $0 && _profile = $1", orderId, - SyncedTaskData.CommunicationProfile.ErxCommunicationDispReq.toEntityValue() + CommunicationProfile.ErxCommunicationDispReq.toEntityValue() ) .asFlow() .map { result -> diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt new file mode 100644 index 00000000..ee85359b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.orders.repository + +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.datetime.Instant + +@Suppress("TooManyFunctions") +interface CommunicationRepository { + + val pharmacyCacheError: MutableSharedFlow + suspend fun downloadCommunications(profileId: ProfileIdentifier): Result + suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> + + suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? + fun loadPharmacies(): Flow> + suspend fun downloadMissingPharmacy(telematikId: String) + fun loadSyncedByTaskId(taskId: String): Flow + fun loadScannedByTaskId(taskId: String): Flow + + fun loadDispReqCommunications(orderId: String): Flow> + fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> + fun loadRepliedCommunications(taskIds: List): Flow> + fun hasUnreadPrescription(taskIds: List, orderId: String): Flow + fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow + fun unreadOrders(profileId: ProfileIdentifier): Flow + fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow + fun taskIdsByOrder(orderId: String): Flow> + suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) + suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt similarity index 60% rename from android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt index 8408adbc..9064e6d3 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/CommunicationRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/DefaultCommunicationRepository.kt @@ -21,36 +21,39 @@ package de.gematik.ti.erp.app.orders.repository import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.api.ResourcePaging import de.gematik.ti.erp.app.fhir.model.extractPharmacyServices +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import de.gematik.ti.erp.app.prescription.repository.LocalDataSource -import de.gematik.ti.erp.app.prescription.repository.RemoteDataSource -import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import kotlinx.datetime.Instant -private const val CommunicationsMaxPageSize = 50 +private const val COMMUNICATION_MAX_PAGE_SIZE = 50 @Suppress("TooManyFunctions") -class CommunicationRepository( - private val taskLocalDataSource: LocalDataSource, - private val taskRemoteDataSource: RemoteDataSource, +class DefaultCommunicationRepository( + private val taskLocalDataSource: PrescriptionLocalDataSource, + private val taskRemoteDataSource: PrescriptionRemoteDataSource, private val communicationLocalDataSource: CommunicationLocalDataSource, private val cacheLocalDataSource: PharmacyCacheLocalDataSource, private val cacheRemoteDataSource: PharmacyCacheRemoteDataSource, private val dispatchers: DispatchProvider -) : ResourcePaging(dispatchers, CommunicationsMaxPageSize) { - private val scope = CoroutineScope(dispatchers.IO) +) : ResourcePaging(dispatchers, COMMUNICATION_MAX_PAGE_SIZE), CommunicationRepository { + private val scope = CoroutineScope(dispatchers.io) private val queue = Channel(capacity = Channel.BUFFERED) - val pharmacyCacheError = MutableSharedFlow() + override val pharmacyCacheError = MutableSharedFlow() init { scope.launch { @@ -73,7 +76,7 @@ class CommunicationRepository( override val tag: String = "CommunicationRepository" - suspend fun downloadCommunications(profileId: ProfileIdentifier) = downloadPaged(profileId) + override suspend fun downloadCommunications(profileId: ProfileIdentifier) = downloadPaged(profileId) override suspend fun downloadResource( profileId: ProfileIdentifier, @@ -93,48 +96,51 @@ class CommunicationRepository( override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = communicationLocalDataSource.latestCommunicationTimestamp(profileId).first() - fun loadPharmacies(): Flow> = - cacheLocalDataSource.loadPharmacies().flowOn(dispatchers.IO) + override fun loadPharmacies(): Flow> = + cacheLocalDataSource.loadPharmacies().flowOn(dispatchers.io) - suspend fun downloadMissingPharmacy(telematikId: String) { + override suspend fun downloadMissingPharmacy(telematikId: String) { queue.send(telematikId) } - fun loadPrescriptionName(taskId: String) = - taskLocalDataSource.loadSyncedTaskByTaskId(taskId).map { - it?.medicationName() - }.flowOn(dispatchers.IO) + override fun loadSyncedByTaskId(taskId: String): Flow = + taskLocalDataSource.loadSyncedTaskByTaskId(taskId) + .flowOn(dispatchers.io) + + override fun loadScannedByTaskId(taskId: String): Flow = + taskLocalDataSource.loadScannedTaskByTaskId(taskId) + .flowOn(dispatchers.io) - fun loadDispReqCommunications(orderId: String) = - communicationLocalDataSource.loadDispReqCommunications(orderId).flowOn(dispatchers.IO) + override fun loadDispReqCommunications(orderId: String): Flow> = + communicationLocalDataSource.loadDispReqCommunications(orderId).flowOn(dispatchers.io) - fun loadFirstDispReqCommunications(profileId: ProfileIdentifier) = - communicationLocalDataSource.loadFirstDispReqCommunications(profileId).flowOn(dispatchers.IO) + override fun loadFirstDispReqCommunications(profileId: ProfileIdentifier): Flow> = + communicationLocalDataSource.loadFirstDispReqCommunications(profileId).flowOn(dispatchers.io) - fun loadRepliedCommunications(taskIds: List) = - communicationLocalDataSource.loadRepliedCommunications(taskIds = taskIds).flowOn(dispatchers.IO) + override fun loadRepliedCommunications(taskIds: List): Flow> = + communicationLocalDataSource.loadRepliedCommunications(taskIds = taskIds).flowOn(dispatchers.io) - fun hasUnreadPrescription(taskIds: List, orderId: String) = - communicationLocalDataSource.hasUnreadPrescription(taskIds, orderId).flowOn(dispatchers.IO) + override fun hasUnreadPrescription(taskIds: List, orderId: String): Flow = + communicationLocalDataSource.hasUnreadPrescription(taskIds, orderId).flowOn(dispatchers.io) - fun hasUnreadPrescription(profileId: ProfileIdentifier) = - communicationLocalDataSource.hasUnreadPrescription(profileId).flowOn(dispatchers.IO) + override fun hasUnreadPrescription(profileId: ProfileIdentifier): Flow = + communicationLocalDataSource.hasUnreadPrescription(profileId).flowOn(dispatchers.io) - fun unreadOrders(profileId: ProfileIdentifier) = - communicationLocalDataSource.unreadOrders(profileId).flowOn(dispatchers.IO) - fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier) = - communicationLocalDataSource.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.IO) + override fun unreadOrders(profileId: ProfileIdentifier): Flow = + communicationLocalDataSource.unreadOrders(profileId).flowOn(dispatchers.io) + override fun unreadPrescriptionsInAllOrders(profileId: ProfileIdentifier): Flow = + communicationLocalDataSource.unreadPrescriptionsInAllOrders(profileId).flowOn(dispatchers.io) - fun taskIdsByOrder(orderId: String) = - communicationLocalDataSource.taskIdsByOrder(orderId).flowOn(dispatchers.IO) + override fun taskIdsByOrder(orderId: String): Flow> = + communicationLocalDataSource.taskIdsByOrder(orderId).flowOn(dispatchers.io) - suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { - withContext(dispatchers.IO) { + override suspend fun setCommunicationStatus(communicationId: String, consumed: Boolean) { + withContext(dispatchers.io) { communicationLocalDataSource.setCommunicationStatus(communicationId, consumed) } } - suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { + override suspend fun saveLocalCommunication(taskId: String, pharmacyId: String, transactionId: String) { taskLocalDataSource.saveLocalCommunication(taskId, pharmacyId, transactionId) } } diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheLocalDataSource.kt diff --git a/android/src/main/java/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt similarity index 100% rename from android/src/main/java/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/orders/repository/PharmacyCacheRemoteDataSource.kt diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt new file mode 100644 index 00000000..2ead9e05 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyLocalDataSource.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.pharmacy.repository + +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.pharmacy.OftenUsedPharmacyEntityV1 +import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 +import de.gematik.ti.erp.app.db.queryFirst +import de.gematik.ti.erp.app.db.toInstant +import de.gematik.ti.erp.app.db.toRealmInstant +import de.gematik.ti.erp.app.db.tryWrite +import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData.OverviewPharmacy +import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData.Pharmacy +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.query.Sort +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.datetime.Clock + +class DefaultPharmacyLocalDataSource(private val realm: Realm) : PharmacyLocalDataSource { + override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { + delete(it) + } + queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { + delete(it) + } + } + } + + override fun loadOftenUsedPharmacies(): Flow> = + realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { + it.list.map { pharmacy -> + pharmacy.toOverviewPharmacy() + } + } + + override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { + this.lastUsed = Clock.System.now().toRealmInstant() + this.usageCount += 1 + } ?: copyToRealm(pharmacy.toOftenUsedPharmacyEntityV1()) + } + } + + override suspend fun deleteFavoritePharmacy(favoritePharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", favoritePharmacy.telematikId)?.let { delete(it) } + } + } + + override fun loadFavoritePharmacies(): Flow> = + realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { + it.list.map { favorite -> + favorite.toOverviewPharmacy() + } + } + + override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: Pharmacy) { + realm.tryWrite { + queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { + this.lastUsed = Clock.System.now().toRealmInstant() + } ?: copyToRealm(pharmacy.toFavoritePharmacyEntityV1()) + } + } + + override fun isPharmacyInFavorites(pharmacy: Pharmacy): Flow = + realm.query("telematikId = $0", pharmacy.telematikId) + .asFlow() + .map { + it.list.isNotEmpty() + } + + override suspend fun markAsRedeemed(taskId: String) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.apply { + this.redeemedOn = Clock.System.now().toRealmInstant() + } + } + } + + companion object { + fun Pharmacy.toOftenUsedPharmacyEntityV1() = + OftenUsedPharmacyEntityV1().apply { + this.address = this@toOftenUsedPharmacyEntityV1.singleLineAddress() + this.pharmacyName = this@toOftenUsedPharmacyEntityV1.name + this.telematikId = this@toOftenUsedPharmacyEntityV1.telematikId + } + + fun OftenUsedPharmacyEntityV1.toOverviewPharmacy() = + OverviewPharmacy( + lastUsed = this.lastUsed.toInstant(), + usageCount = this.usageCount, + isFavorite = false, + telematikId = this.telematikId, + pharmacyName = this.pharmacyName, + address = this.address + ) + + fun Pharmacy.toFavoritePharmacyEntityV1() = + FavoritePharmacyEntityV1().apply { + this.address = this@toFavoritePharmacyEntityV1.singleLineAddress() + this.pharmacyName = this@toFavoritePharmacyEntityV1.name + this.telematikId = this@toFavoritePharmacyEntityV1.telematikId + } + + fun FavoritePharmacyEntityV1.toOverviewPharmacy() = + OverviewPharmacy( + lastUsed = this.lastUsed.toInstant(), + telematikId = this.telematikId, + pharmacyName = this.pharmacyName, + address = this.address, + isFavorite = true, + usageCount = 0 + ) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt index cc0acbf5..ad5e00d1 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/DefaultPharmacyRepository.kt @@ -55,7 +55,7 @@ class DefaultPharmacyRepository( offset: Int, count: Int ): Result = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { remoteDataSource.searchPharmaciesContinued( bundleId = bundleId, offset = offset, @@ -75,7 +75,7 @@ class DefaultPharmacyRepository( override suspend fun searchBinaryCerts( locationId: String ): Result> = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { remoteDataSource.searchBinaryCert( locationId = locationId ).map { @@ -99,31 +99,31 @@ class DefaultPharmacyRepository( ) override fun loadOftenUsedPharmacies() = - localDataSource.loadOftenUsedPharmacies().flowOn(dispatchers.IO) + localDataSource.loadOftenUsedPharmacies().flowOn(dispatchers.io) override fun loadFavoritePharmacies() = - localDataSource.loadFavoritePharmacies().flowOn(dispatchers.IO) + localDataSource.loadFavoritePharmacies().flowOn(dispatchers.io) override suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { localDataSource.saveOrUpdateOftenUsedPharmacy(pharmacy) } } override suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { localDataSource.deleteOverviewPharmacy(overviewPharmacy) } } override suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { localDataSource.saveOrUpdateFavoritePharmacy(pharmacy) } } override suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { localDataSource.deleteFavoritePharmacy(favoritePharmacy) } } @@ -131,7 +131,7 @@ class DefaultPharmacyRepository( override suspend fun searchPharmacyByTelematikId( telematikId: String ): Result = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { remoteDataSource.searchPharmacyByTelematikId(telematikId) .map { extractPharmacyServices( @@ -144,7 +144,7 @@ class DefaultPharmacyRepository( } override fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = - localDataSource.isPharmacyInFavorites(pharmacy).flowOn(dispatchers.IO) + localDataSource.isPharmacyInFavorites(pharmacy).flowOn(dispatchers.io) override suspend fun markAsRedeemed(taskId: String) { localDataSource.markAsRedeemed(taskId) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt index 9c1fc852..66d108ee 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/PharmacyLocalDataSource.kt @@ -18,119 +18,17 @@ package de.gematik.ti.erp.app.pharmacy.repository -import de.gematik.ti.erp.app.db.entities.v1.pharmacy.FavoritePharmacyEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.pharmacy.OftenUsedPharmacyEntityV1 -import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 -import de.gematik.ti.erp.app.db.queryFirst -import de.gematik.ti.erp.app.db.toInstant -import de.gematik.ti.erp.app.db.toRealmInstant -import de.gematik.ti.erp.app.db.tryWrite import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData -import io.realm.kotlin.Realm -import io.realm.kotlin.ext.query -import io.realm.kotlin.query.Sort import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.datetime.Clock -class PharmacyLocalDataSource( - private val realm: Realm -) { - suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { - delete(it) - } - queryFirst("telematikId = $0", overviewPharmacy.telematikId)?.let { - delete(it) - } - } - } - - fun loadOftenUsedPharmacies(): Flow> = - realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { - it.list.map { pharmacy -> - pharmacy.toOverviewPharmacy() - } - } - - suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Clock.System.now().toRealmInstant() - this.usageCount += 1 - } ?: copyToRealm(pharmacy.toOftenUsedPharmacyEntityV1()) - } - } - - fun PharmacyUseCaseData.Pharmacy.toOftenUsedPharmacyEntityV1() = - OftenUsedPharmacyEntityV1().apply { - this.address = this@toOftenUsedPharmacyEntityV1.singleLineAddress() - this.pharmacyName = this@toOftenUsedPharmacyEntityV1.name - this.telematikId = this@toOftenUsedPharmacyEntityV1.telematikId - } - - fun OftenUsedPharmacyEntityV1.toOverviewPharmacy() = - OverviewPharmacyData.OverviewPharmacy( - lastUsed = this.lastUsed.toInstant(), - usageCount = this.usageCount, - isFavorite = false, - telematikId = this.telematikId, - pharmacyName = this.pharmacyName, - address = this.address - ) - - suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", favoritePharmacy.telematikId)?.let { delete(it) } - } - } - - fun loadFavoritePharmacies(): Flow> = - realm.query().sort("lastUsed", Sort.DESCENDING).asFlow().map { - it.list.map { favorite -> - favorite.toOverviewPharmacy() - } - } - - suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) { - realm.tryWrite { - queryFirst("telematikId = $0", pharmacy.telematikId)?.apply { - this.lastUsed = Clock.System.now().toRealmInstant() - } ?: copyToRealm(pharmacy.toFavoritePharmacyEntityV1()) - } - } - - fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow = - realm.query("telematikId = $0", pharmacy.telematikId) - .asFlow() - .map { - it.list.isNotEmpty() - } - - fun PharmacyUseCaseData.Pharmacy.toFavoritePharmacyEntityV1() = - FavoritePharmacyEntityV1().apply { - this.address = this@toFavoritePharmacyEntityV1.singleLineAddress() - this.pharmacyName = this@toFavoritePharmacyEntityV1.name - this.telematikId = this@toFavoritePharmacyEntityV1.telematikId - } - - fun FavoritePharmacyEntityV1.toOverviewPharmacy() = - OverviewPharmacyData.OverviewPharmacy( - lastUsed = this.lastUsed.toInstant(), - telematikId = this.telematikId, - pharmacyName = this.pharmacyName, - address = this.address, - isFavorite = true, - usageCount = 0 - ) - - suspend fun markAsRedeemed(taskId: String) { - realm.tryWrite { - queryFirst("taskId = $0", taskId)?.apply { - this.redeemedOn = Clock.System.now().toRealmInstant() - } - } - } +interface PharmacyLocalDataSource { + suspend fun deleteOverviewPharmacy(overviewPharmacy: OverviewPharmacyData.OverviewPharmacy) + fun loadOftenUsedPharmacies(): Flow> + suspend fun saveOrUpdateOftenUsedPharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) + suspend fun deleteFavoritePharmacy(favoritePharmacy: PharmacyUseCaseData.Pharmacy) + fun loadFavoritePharmacies(): Flow> + suspend fun saveOrUpdateFavoritePharmacy(pharmacy: PharmacyUseCaseData.Pharmacy) + fun isPharmacyInFavorites(pharmacy: PharmacyUseCaseData.Pharmacy): Flow + suspend fun markAsRedeemed(taskId: String) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt index 93ba5d03..cff87151 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/repository/ShippingContactRepository.kt @@ -41,10 +41,10 @@ class ShippingContactRepository( .map { it.obj?.toShippingContact() } - .flowOn(dispatchers.IO) + .flowOn(dispatchers.io) suspend fun saveShippingContact(contact: PharmacyData.ShippingContact) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { realm.write { queryFirst()?.let { settings -> val shippingContact = settings.shippingContact diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt index 4d01a999..0c7e50c4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyMapsUseCase.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.pharmacy.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.pharmacy.usecase.mapper.PharmacyInitialResultsPerPage -import de.gematik.ti.erp.app.pharmacy.usecase.mapper.mapToUseCasePharmacies +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import de.gematik.ti.erp.app.settings.model.SettingsData @@ -38,7 +38,7 @@ class PharmacyMapsUseCase( suspend fun searchPharmacies( searchData: PharmacyUseCaseData.SearchData ): List = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { settingsRepository.savePharmacySearch( SettingsData.PharmacySearch( name = searchData.name, @@ -71,7 +71,7 @@ class PharmacyMapsUseCase( ).getOrThrow() if (initialResult.bundleResultCount == PharmacyInitialResultsPerPage) { - val pharmacies = initialResult.pharmacies.mapToUseCasePharmacies().toMutableList() + val pharmacies = initialResult.pharmacies.toModel().toMutableList() var offset = initialResult.bundleResultCount loop@ while (true) { @@ -85,13 +85,13 @@ class PharmacyMapsUseCase( break@loop } - pharmacies += result.pharmacies.mapToUseCasePharmacies() + pharmacies += result.pharmacies.toModel() offset += result.bundleResultCount } pharmacies } else { - initialResult.pharmacies.mapToUseCasePharmacies() + initialResult.pharmacies.toModel() } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt index c19d435f..c850e55c 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/PharmacyOverviewUseCase.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.pharmacy.usecase import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.pharmacy.model.OverviewPharmacyData -import de.gematik.ti.erp.app.pharmacy.usecase.mapper.mapToUseCasePharmacies +import de.gematik.ti.erp.app.pharmacy.usecase.mapper.toModel import de.gematik.ti.erp.app.pharmacy.repository.PharmacyRepository import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData import kotlinx.coroutines.flow.Flow @@ -32,10 +32,10 @@ class PharmacyOverviewUseCase( private val dispatchers: DispatchProvider ) { fun oftenUsedPharmacies(): Flow> = - repository.loadOftenUsedPharmacies().flowOn(dispatchers.IO) + repository.loadOftenUsedPharmacies().flowOn(dispatchers.io) fun favoritePharmacies(): Flow> = - repository.loadFavoritePharmacies().flowOn(dispatchers.IO) + repository.loadFavoritePharmacies().flowOn(dispatchers.io) suspend fun saveOrUpdateUsedPharmacies(pharmacy: PharmacyUseCaseData.Pharmacy) { repository.saveOrUpdateOftenUsedPharmacy(pharmacy) @@ -47,8 +47,8 @@ class PharmacyOverviewUseCase( suspend fun searchPharmacyByTelematikId( telematikId: String - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { repository.searchPharmacyByTelematikId(telematikId) - .map { it.pharmacies.mapToUseCasePharmacies().firstOrNull() } + .map { it.pharmacies.toModel().firstOrNull() } } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt index c271f1c7..54ad61d6 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/mapper/PharmacyUseCaseDataMapper.kt @@ -19,13 +19,14 @@ package de.gematik.ti.erp.app.pharmacy.usecase.mapper import de.gematik.ti.erp.app.fhir.model.LocalPharmacyService import de.gematik.ti.erp.app.fhir.model.Pharmacy +import de.gematik.ti.erp.app.pharmacy.model.PharmacyData import de.gematik.ti.erp.app.pharmacy.usecase.model.PharmacyUseCaseData // can't be modified; the backend will always return 80 entries on the first page const val PharmacyInitialResultsPerPage = 80 const val PharmacyNextResultsPerPage = 10 -fun List.mapToUseCasePharmacies(): List = +fun List.toModel(): List = map { pharmacy -> PharmacyUseCaseData.Pharmacy( id = pharmacy.id, @@ -37,7 +38,23 @@ fun List.mapToUseCasePharmacies(): List distance = null, contacts = pharmacy.contacts, provides = pharmacy.provides, - openingHours = (pharmacy.provides.find { it is LocalPharmacyService } as LocalPharmacyService).openingHours, + openingHours = ( + pharmacy.provides.find { + it is LocalPharmacyService + } as? LocalPharmacyService + )?.openingHours, telematikId = pharmacy.telematikId ) } + +fun PharmacyData.ShippingContact.toModel() = + PharmacyUseCaseData.ShippingContact( + name = name, + line1 = line1, + line2 = line2, + postalCode = postalCode, + city = city, + telephoneNumber = telephoneNumber, + mail = mail, + deliveryInformation = deliveryInformation + ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt index 5f986e2d..d09b9abe 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/pharmacy/usecase/model/PharmacyUseCaseData.kt @@ -58,6 +58,14 @@ object PharmacyUseCaseData { val openingHours: OpeningHours?, val telematikId: String ) { + val isPickupService + get() = provides.any { it is PickUpPharmacyService } + + val isDeliveryService + get() = provides.any { it is DeliveryPharmacyService } + + val isOnlineService + get() = provides.any { it is OnlinePharmacyService } @Stable fun singleLineAddress(): String = @@ -66,18 +74,6 @@ object PharmacyUseCaseData { } else { address.replace("\n", ", ") } - - @Stable - fun pickupServiceAvailable(): Boolean = - provides.any { it is PickUpPharmacyService } - - @Stable - fun deliveryServiceAvailable(): Boolean = - provides.any { it is DeliveryPharmacyService } - - @Stable - fun onlineServiceAvailable(): Boolean = - provides.any { it is OnlinePharmacyService } } sealed class LocationMode { @@ -85,10 +81,10 @@ object PharmacyUseCaseData { * We only store the information if gps was enabled and not the actual position. */ @Immutable - object EnabledWithoutPosition : LocationMode() + data object EnabledWithoutPosition : LocationMode() @Immutable - object Disabled : LocationMode() + data object Disabled : LocationMode() @Immutable data class Enabled(val location: Location, val radiusInMeter: Double = DefaultRadiusInMeter) : LocationMode() @@ -110,6 +106,7 @@ object PharmacyUseCaseData { val taskId: String, val accessCode: String, val title: String?, + val index: Int?, val timestamp: Instant, val substitutionsAllowed: Boolean ) @@ -159,7 +156,7 @@ object PharmacyUseCaseData { fun addressIsMissing() = name.isBlank() || line1.isBlank() || postalCode.isBlank() || city.isBlank() companion object { - val Empty = ShippingContact( + val EmptyShippingContact = ShippingContact( name = "", line1 = "", line2 = "", @@ -174,13 +171,13 @@ object PharmacyUseCaseData { @Immutable data class OrderState( - val prescriptions: List, + val orders: List, val contact: ShippingContact ) { companion object { val Empty = OrderState( - prescriptions = emptyList(), - contact = ShippingContact.Empty + orders = emptyList(), + contact = ShippingContact.EmptyShippingContact ) } } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt index 075ad776..5f3973ce 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/TaskModule.kt @@ -18,6 +18,7 @@ package de.gematik.ti.erp.app.prescription +import de.gematik.ti.erp.app.prescription.repository.DefaultTaskRepository import de.gematik.ti.erp.app.prescription.repository.TaskLocalDataSource import de.gematik.ti.erp.app.prescription.repository.TaskRemoteDataSource import de.gematik.ti.erp.app.prescription.repository.TaskRepository @@ -26,7 +27,10 @@ import org.kodein.di.bindProvider import org.kodein.di.instance val taskModule = DI.Module("taskModule") { - bindProvider { TaskRepository(instance(), instance(), instance()) } bindProvider { TaskRemoteDataSource(instance()) } bindProvider { TaskLocalDataSource(instance()) } } + +val taskRepositoryModule = DI.Module("taskRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultTaskRepository(instance(), instance(), instance()) } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt new file mode 100644 index 00000000..37856c05 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/Communication.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.model + +import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 +import kotlinx.datetime.Instant + +data class Communication( + val taskId: String, + val communicationId: String, + val orderId: String, + val profile: CommunicationProfile, + val sentOn: Instant, + val sender: String, + val recipient: String, + val payload: String?, + val consumed: Boolean +) + +enum class CommunicationProfile { + ErxCommunicationDispReq, ErxCommunicationReply; + + fun toEntityValue() = when (this) { + ErxCommunicationDispReq -> + CommunicationProfileV1.ErxCommunicationDispReq + + ErxCommunicationReply -> + CommunicationProfileV1.ErxCommunicationReply + }.name +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt index dc08cb13..c09295bd 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/ScannedTaskData.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.prescription.model -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationEntityV1 import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.datetime.Instant @@ -26,10 +25,12 @@ object ScannedTaskData { data class ScannedTask( val profileId: ProfileIdentifier, val taskId: String, + val index: Int, + val name: String?, val accessCode: String, val scannedOn: Instant, val redeemedOn: Instant?, - val communications: List = emptyList() + val communications: List = emptyList() ) { fun isRedeemable() = redeemedOn == null } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt index d425a3ba..58383fe5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/model/SyncedTaskData.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.prescription.model -import de.gematik.ti.erp.app.db.entities.v1.task.CommunicationProfileV1 import de.gematik.ti.erp.app.utils.FhirTemporal import de.gematik.ti.erp.app.utils.toStartOfDayInUTC import kotlinx.datetime.Clock @@ -38,18 +37,6 @@ object SyncedTaskData { Ready, InProgress, Completed, Other, Draft, Requested, Received, Accepted, Rejected, Canceled, OnHold, Failed; } - enum class CommunicationProfile { - ErxCommunicationDispReq, ErxCommunicationReply; - - fun toEntityValue() = when (this) { - ErxCommunicationDispReq -> - CommunicationProfileV1.ErxCommunicationDispReq - - ErxCommunicationReply -> - CommunicationProfileV1.ErxCommunicationReply - }.name - } - data class SyncedTask( val profileId: String, val taskId: String, @@ -381,18 +368,6 @@ object SyncedTaskData { override fun name() = text } - data class Communication( - val taskId: String, - val communicationId: String, - val orderId: String, - val profile: CommunicationProfile, - val sentOn: Instant, - val sender: String, - val recipient: String, - val payload: String?, - val consumed: Boolean - ) - fun joinIngredientNames(ingredients: List) = ingredients.joinToString(", ") { ingredient -> ingredient.text diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt similarity index 64% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt index 193a9abc..f00fd43a 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultPrescriptionRepository.kt @@ -26,8 +26,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext -import kotlinx.serialization.json.JsonElement import kotlinx.datetime.Instant +import kotlinx.serialization.json.JsonElement enum class RemoteRedeemOption(val type: String) { Local(type = "onPremise"), @@ -35,39 +35,39 @@ enum class RemoteRedeemOption(val type: String) { Delivery(type = "delivery") } -class PrescriptionRepository( +class DefaultPrescriptionRepository( private val dispatchers: DispatchProvider, - private val localDataSource: LocalDataSource, - private val remoteDataSource: RemoteDataSource -) { + private val localDataSource: PrescriptionLocalDataSource, + private val remoteDataSource: PrescriptionRemoteDataSource +) : PrescriptionRepository { /** * Saves all scanned tasks. It doesn't matter if they already exist. */ - suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) { - withContext(dispatchers.IO) { + override suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) { + withContext(dispatchers.io) { localDataSource.saveScannedTasks(profileId, tasks) } } - fun scannedTasks(profileId: ProfileIdentifier) = + override fun scannedTasks(profileId: ProfileIdentifier) = localDataSource.loadScannedTasks(profileId) - fun syncedTasks(profileId: ProfileIdentifier) = + override fun syncedTasks(profileId: ProfileIdentifier) = localDataSource.loadSyncedTasks(profileId) - suspend fun redeemPrescription( + override suspend fun redeemPrescription( profileId: ProfileIdentifier, communication: JsonElement, - accessCode: String? = null - ): Result = withContext(dispatchers.IO) { + accessCode: String? + ): Result = withContext(dispatchers.io) { remoteDataSource.communicate(profileId, communication, accessCode).map { } } - suspend fun deleteTaskByTaskId( + override suspend fun deleteTaskByTaskId( profileId: ProfileIdentifier, taskId: String - ): Result = withContext(dispatchers.IO) { + ): Result = withContext(dispatchers.io) { // check if task is local only if (localDataSource.loadScannedTaskByTaskId(taskId).first() != null) { localDataSource.deleteTask(taskId) @@ -79,15 +79,17 @@ class PrescriptionRepository( } } - suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) = withContext(dispatchers.IO) { + override suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) = localDataSource.updateRedeemedOn(taskId, timestamp) - } - fun loadSyncedTaskByTaskId(taskId: String): Flow = - localDataSource.loadSyncedTaskByTaskId(taskId).flowOn(dispatchers.IO) + override suspend fun updateScannedTaskName(taskId: String, name: String) = + localDataSource.updateScannedTaskName(taskId, name) + + override fun loadSyncedTaskByTaskId(taskId: String): Flow = + localDataSource.loadSyncedTaskByTaskId(taskId) - fun loadScannedTaskByTaskId(taskId: String): Flow = - localDataSource.loadScannedTaskByTaskId(taskId).flowOn(dispatchers.IO) + override fun loadScannedTaskByTaskId(taskId: String): Flow = + localDataSource.loadScannedTaskByTaskId(taskId) - fun loadTaskIds(): Flow> = localDataSource.loadTaskIds().flowOn(dispatchers.IO) + override fun loadTaskIds(): Flow> = localDataSource.loadTaskIds().flowOn(dispatchers.io) } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt new file mode 100644 index 00000000..7a1715b0 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DefaultTaskRepository.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.repository + +import de.gematik.ti.erp.app.DispatchProvider +import de.gematik.ti.erp.app.api.ResourcePaging +import de.gematik.ti.erp.app.fhir.model.extractTaskIds +import de.gematik.ti.erp.app.fhir.parser.findAll +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import kotlinx.datetime.Instant + +private const val TasksMaxPageSize = 50 + +class DefaultTaskRepository( + private val remoteDataSource: TaskRemoteDataSource, + private val localDataSource: TaskLocalDataSource, + private val dispatchers: DispatchProvider +) : ResourcePaging(dispatchers, TasksMaxPageSize, maxPages = 1), TaskRepository { + + override suspend fun downloadTasks(profileId: ProfileIdentifier) = + downloadPaged(profileId) { prev: Int?, next: Int -> + (prev ?: 0) + next + }.map { + it ?: 0 + } + + override val tag: String = javaClass::getSimpleName.name + + override suspend fun downloadResource( + profileId: ProfileIdentifier, + timestamp: String?, + count: Int? + ): Result> = + remoteDataSource.getTasks( + profileId = profileId, + lastUpdated = timestamp, + count = count + ).mapCatching { fhirBundle -> + withContext(dispatchers.io) { + val (total, taskIds) = extractTaskIds(fhirBundle) + + supervisorScope { + val results = taskIds.map { taskId -> + async { + downloadTaskWithKBVBundle(taskId = taskId, profileId = profileId).map { + if (it.isCompleted) { + downloadMedicationDispenses( + profileId, + taskId + ) + } + + requireNotNull(it.lastModified) + } + } + }.awaitAll() + + // throw if any result is not parsed correctly + results.find { it.isFailure }?.getOrThrow() + + // return number of bundles saved to db + ResourceResult(total, results.size) + } + } + } + + private suspend fun downloadTaskWithKBVBundle( + taskId: String, + profileId: ProfileIdentifier + ): Result = withContext(dispatchers.io) { + remoteDataSource.taskWithKBVBundle(profileId, taskId).mapCatching { bundle -> + requireNotNull(localDataSource.saveTask(profileId, bundle)) + } + } + + private suspend fun downloadMedicationDispenses( + profileId: ProfileIdentifier, + taskId: String + ): Result = withContext(dispatchers.io) { + remoteDataSource.loadBundleOfMedicationDispenses(profileId, taskId).map { bundle -> + bundle.findAll("entry.resource") + .forEach { dispense -> + localDataSource.saveMedicationDispense(taskId, dispense) + } + } + } + + override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = + localDataSource.latestTaskModifiedTimestamp(profileId).first() +} diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt similarity index 94% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt index 8e439416..016aab59 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/LocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionLocalDataSource.kt @@ -43,7 +43,7 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.json.JsonElement -class LocalDataSource( +class PrescriptionLocalDataSource( private val realm: Realm ) { suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) { @@ -57,6 +57,8 @@ class LocalDataSource( ) { profile.scannedTasks += copyToRealm( ScannedTaskEntityV1().apply { + this.index = task.index + 1 + this.name = task.name this.parent = profile this.taskId = task.taskId this.accessCode = task.accessCode @@ -149,6 +151,7 @@ class LocalDataSource( fun loadScannedTasks(profileId: ProfileIdentifier): Flow> = realm.query("parent.id = $0", profileId) .sort("scannedOn", Sort.DESCENDING) + .sort("index", Sort.ASCENDING) .asFlow() .map { scannedTasks -> scannedTasks.list.map { task -> @@ -178,6 +181,14 @@ class LocalDataSource( } } + suspend fun updateScannedTaskName(taskId: String, name: String) { + realm.tryWrite { + queryFirst("taskId = $0", taskId)?.apply { + this.name = name + } + } + } + fun loadTaskIds(): Flow> = combine( realm.query().asFlow(), diff --git a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt similarity index 98% rename from android/src/main/java/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt index b2b452c2..f6c52e1b 100644 --- a/android/src/main/java/de/gematik/ti/erp/app/prescription/repository/RemoteDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRemoteDataSource.kt @@ -24,7 +24,7 @@ import de.gematik.ti.erp.app.api.safeApiCallNullable import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.serialization.json.JsonElement -class RemoteDataSource( +class PrescriptionRemoteDataSource( private val service: ErpService ) { suspend fun fetchCommunications( diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt new file mode 100644 index 00000000..6f8a5e1b --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.prescription.repository + +import de.gematik.ti.erp.app.prescription.model.ScannedTaskData +import de.gematik.ti.erp.app.prescription.model.SyncedTaskData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant +import kotlinx.serialization.json.JsonElement + +interface PrescriptionRepository { + suspend fun saveScannedTasks(profileId: ProfileIdentifier, tasks: List) + + fun scannedTasks(profileId: ProfileIdentifier): Flow> + + fun syncedTasks(profileId: ProfileIdentifier): Flow> + + suspend fun redeemPrescription( + profileId: ProfileIdentifier, + communication: JsonElement, + accessCode: String? = null + ): Result + + suspend fun deleteTaskByTaskId( + profileId: ProfileIdentifier, + taskId: String + ): Result + + suspend fun updateRedeemedOn(taskId: String, timestamp: Instant?) + + suspend fun updateScannedTaskName(taskId: String, name: String) + + fun loadSyncedTaskByTaskId(taskId: String): Flow + + fun loadScannedTaskByTaskId(taskId: String): Flow + + fun loadTaskIds(): Flow> +} 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 fcf5a27d..c8e057ef 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskLocalDataSource.kt @@ -51,10 +51,12 @@ import de.gematik.ti.erp.app.fhir.model.extractKBVBundle import de.gematik.ti.erp.app.fhir.model.extractMedicationDispense import de.gematik.ti.erp.app.fhir.model.extractTask import de.gematik.ti.erp.app.fhir.model.extractTaskAndKBVBundle -import de.gematik.ti.erp.app.utils.FhirTemporal +import de.gematik.ti.erp.app.prescription.model.Communication +import de.gematik.ti.erp.app.prescription.model.CommunicationProfile import de.gematik.ti.erp.app.prescription.model.ScannedTaskData import de.gematik.ti.erp.app.prescription.model.SyncedTaskData import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.utils.FhirTemporal import io.realm.kotlin.Realm import io.realm.kotlin.ext.query import io.realm.kotlin.ext.toRealmList @@ -532,6 +534,7 @@ fun SyncedTaskEntityV1.toSyncedTask(): SyncedTaskData.SyncedTask = communication.toCommunication() } ) + fun MedicationEntityV1?.toMedication(): SyncedTaskData.Medication? = when (this?.medicationProfile) { MedicationProfileV1.PZN -> SyncedTaskData.MedicationPZN( @@ -624,16 +627,16 @@ fun CommunicationEntityV1.toCommunication() = if (this.profile == CommunicationProfileV1.Unknown) { null } else { - SyncedTaskData.Communication( + Communication( taskId = this.taskId, communicationId = this.communicationId, orderId = this.orderId, profile = when (this.profile) { CommunicationProfileV1.ErxCommunicationDispReq -> - SyncedTaskData.CommunicationProfile.ErxCommunicationDispReq + CommunicationProfile.ErxCommunicationDispReq CommunicationProfileV1.ErxCommunicationReply -> - SyncedTaskData.CommunicationProfile.ErxCommunicationReply + CommunicationProfile.ErxCommunicationReply else -> error("should not happen") }, @@ -649,8 +652,10 @@ fun ScannedTaskEntityV1.toScannedTask() = ScannedTaskData.ScannedTask( profileId = this.parent!!.id, taskId = this.taskId, + name = this.name, + index = this.index, accessCode = this.accessCode, scannedOn = this.scannedOn.toInstant(), redeemedOn = this.redeemedOn?.toInstant(), - communications = this.communications + communications = this.communications.mapNotNull { it.toCommunication() } ) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt index 14abb896..3c0be663 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/prescription/repository/TaskRepository.kt @@ -18,93 +18,19 @@ package de.gematik.ti.erp.app.prescription.repository -import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.api.ResourcePaging -import de.gematik.ti.erp.app.fhir.model.extractTaskIds -import de.gematik.ti.erp.app.fhir.parser.findAll import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.supervisorScope -import kotlinx.coroutines.withContext import kotlinx.datetime.Instant -private const val TasksMaxPageSize = 50 +interface TaskRepository { -class TaskRepository( - private val remoteDataSource: TaskRemoteDataSource, - private val localDataSource: TaskLocalDataSource, - private val dispatchers: DispatchProvider -) : ResourcePaging(dispatchers, TasksMaxPageSize, maxPages = 1) { + suspend fun downloadTasks(profileId: ProfileIdentifier): Result - suspend fun downloadTasks(profileId: ProfileIdentifier) = downloadPaged(profileId) { prev: Int?, next: Int -> - (prev ?: 0) + next - }.map { - it ?: 0 - } - - override val tag: String = "TaskRepository" - - override suspend fun downloadResource( + suspend fun downloadResource( profileId: ProfileIdentifier, timestamp: String?, count: Int? - ): Result> = - remoteDataSource.getTasks( - profileId = profileId, - lastUpdated = timestamp, - count = count - ).mapCatching { fhirBundle -> - withContext(dispatchers.IO) { - val (total, taskIds) = extractTaskIds(fhirBundle) - - supervisorScope { - val results = taskIds.map { taskId -> - async { - downloadTaskWithKBVBundle(taskId = taskId, profileId = profileId).map { - if (it.isCompleted) { - downloadMedicationDispenses( - profileId, - taskId - ) - } - - requireNotNull(it.lastModified) - } - } - }.awaitAll() - - // throw if any result is not parsed correctly - results.find { it.isFailure }?.getOrThrow() - - // return number of bundles saved to db - ResourceResult(total, results.size) - } - } - } - - private suspend fun downloadTaskWithKBVBundle( - taskId: String, - profileId: ProfileIdentifier - ): Result = withContext(dispatchers.IO) { - remoteDataSource.taskWithKBVBundle(profileId, taskId).mapCatching { bundle -> - requireNotNull(localDataSource.saveTask(profileId, bundle)) - } - } - - private suspend fun downloadMedicationDispenses( - profileId: ProfileIdentifier, - taskId: String - ): Result = withContext(dispatchers.IO) { - remoteDataSource.loadBundleOfMedicationDispenses(profileId, taskId).map { bundle -> - bundle.findAll("entry.resource") - .forEach { dispense -> - localDataSource.saveMedicationDispense(taskId, dispense) - } - } - } + ): Result> - override suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? = - localDataSource.latestTaskModifiedTimestamp(profileId).first() + suspend fun syncedUpTo(profileId: ProfileIdentifier): Instant? } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt index 9f24b686..56d391f5 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/model/ProfilesData.kt @@ -24,7 +24,7 @@ import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import kotlinx.datetime.Instant object ProfilesData { - enum class AvatarFigure { + enum class Avatar { PersonalizedImage, FemaleDoctor, WomanWithHeadScarf, @@ -40,16 +40,16 @@ object ProfilesData { FemaleDoctorWithPhone, FemaleDeveloper } - class Profile( + data class Profile( val id: ProfileIdentifier, val color: ProfileColorNames, - val avatarFigure: AvatarFigure, + val avatar: Avatar, val personalizedImage: ByteArray? = null, val name: String, val insurantName: String? = null, val insuranceIdentifier: String? = null, val insuranceName: String? = null, - val insuranceType: InsuranceTypeV1, + val insuranceType: InsuranceTypeV1, // TODO: Map to domain insurance type val lastAuthenticated: Instant? = null, val lastAuditEventSynced: Instant? = null, val lastTaskSynced: Instant? = null, @@ -68,7 +68,7 @@ object ProfilesData { other as Profile if (id != other.id) return false - if (avatarFigure != other.avatarFigure) return false + if (avatar != other.avatar) return false if (personalizedImage != null) { if (other.personalizedImage == null) return false if (!personalizedImage.contentEquals(other.personalizedImage)) return false @@ -88,7 +88,7 @@ object ProfilesData { override fun hashCode(): Int { var result = id.hashCode() result = 31 * result + color.hashCode() - result = 31 * result + avatarFigure.hashCode() + result = 31 * result + avatar.hashCode() result = 31 * result + (personalizedImage?.contentHashCode() ?: 0) result = 31 * result + name.hashCode() result = 31 * result + (insurantName?.hashCode() ?: 0) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt similarity index 75% rename from common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt rename to common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt index 98e26667..6cc156b0 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/DefaultProfilesRepository.kt @@ -31,12 +31,14 @@ import de.gematik.ti.erp.app.idp.repository.toSingleSignOnTokenScope import de.gematik.ti.erp.app.profiles.model.ProfilesData import io.realm.kotlin.Realm import io.realm.kotlin.ext.query +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Instant +// TODO: Move to value class typealias ProfileIdentifier = String class KVNRAlreadyAssignedException( @@ -46,13 +48,13 @@ class KVNRAlreadyAssignedException( val insuranceIdentifier: String ) : IllegalStateException(message) -class ProfilesRepository constructor( +class DefaultProfilesRepository constructor( private val dispatchers: DispatchProvider, private val realm: Realm -) { +) : ProfileRepository { private val lock = Mutex() - fun profiles() = + override fun profiles() = realm.query().asFlow().mapNotNull { val hasActiveProfile = it.list.any { profile -> profile.active } @@ -66,21 +68,21 @@ class ProfilesRepository constructor( ProfileColorNamesV1.TREE -> ProfilesData.ProfileColorNames.TREE ProfileColorNamesV1.BLUE_MOON -> ProfilesData.ProfileColorNames.BLUE_MOON }, - avatarFigure = when (profile.avatarFigure) { - AvatarFigureV1.PersonalizedImage -> ProfilesData.AvatarFigure.PersonalizedImage - AvatarFigureV1.FemaleDoctor -> ProfilesData.AvatarFigure.FemaleDoctor - AvatarFigureV1.WomanWithHeadScarf -> ProfilesData.AvatarFigure.WomanWithHeadScarf - AvatarFigureV1.Grandfather -> ProfilesData.AvatarFigure.Grandfather - AvatarFigureV1.BoyWithHealthCard -> ProfilesData.AvatarFigure.BoyWithHealthCard - AvatarFigureV1.OldManOfColor -> ProfilesData.AvatarFigure.OldManOfColor - AvatarFigureV1.WomanWithPhone -> ProfilesData.AvatarFigure.WomanWithPhone - AvatarFigureV1.Grandmother -> ProfilesData.AvatarFigure.Grandmother - AvatarFigureV1.ManWithPhone -> ProfilesData.AvatarFigure.ManWithPhone - AvatarFigureV1.WheelchairUser -> ProfilesData.AvatarFigure.WheelchairUser - AvatarFigureV1.Baby -> ProfilesData.AvatarFigure.Baby - AvatarFigureV1.MaleDoctorWithPhone -> ProfilesData.AvatarFigure.MaleDoctorWithPhone - AvatarFigureV1.FemaleDoctorWithPhone -> ProfilesData.AvatarFigure.FemaleDoctorWithPhone - AvatarFigureV1.FemaleDeveloper -> ProfilesData.AvatarFigure.FemaleDeveloper + avatar = when (profile.avatarFigure) { + AvatarFigureV1.PersonalizedImage -> ProfilesData.Avatar.PersonalizedImage + AvatarFigureV1.FemaleDoctor -> ProfilesData.Avatar.FemaleDoctor + AvatarFigureV1.WomanWithHeadScarf -> ProfilesData.Avatar.WomanWithHeadScarf + AvatarFigureV1.Grandfather -> ProfilesData.Avatar.Grandfather + AvatarFigureV1.BoyWithHealthCard -> ProfilesData.Avatar.BoyWithHealthCard + AvatarFigureV1.OldManOfColor -> ProfilesData.Avatar.OldManOfColor + AvatarFigureV1.WomanWithPhone -> ProfilesData.Avatar.WomanWithPhone + AvatarFigureV1.Grandmother -> ProfilesData.Avatar.Grandmother + AvatarFigureV1.ManWithPhone -> ProfilesData.Avatar.ManWithPhone + AvatarFigureV1.WheelchairUser -> ProfilesData.Avatar.WheelchairUser + AvatarFigureV1.Baby -> ProfilesData.Avatar.Baby + AvatarFigureV1.MaleDoctorWithPhone -> ProfilesData.Avatar.MaleDoctorWithPhone + AvatarFigureV1.FemaleDoctorWithPhone -> ProfilesData.Avatar.FemaleDoctorWithPhone + AvatarFigureV1.FemaleDeveloper -> ProfilesData.Avatar.FemaleDeveloper }, personalizedImage = profile.personalizedImage, name = profile.name, @@ -101,9 +103,14 @@ class ProfilesRepository constructor( ) } - }.flowOn(dispatchers.Main) + }.flowOn(dispatchers.main) - suspend fun saveProfile(profileName: String, activate: Boolean) { + override fun activeProfile(): Flow = + profiles().mapNotNull { + it.find { profile -> profile.active } + } + + override suspend fun saveProfile(profileName: String, activate: Boolean) { realm.write { if (activate) { query().find().forEach { @@ -121,7 +128,7 @@ class ProfilesRepository constructor( } // tag::SwitchActiveProfileRepository[] - suspend fun activateProfile(profileId: ProfileIdentifier) { + override suspend fun activateProfile(profileId: ProfileIdentifier) { realm.write { query("id != $0", profileId).find().forEach { it.active = false @@ -133,7 +140,7 @@ class ProfilesRepository constructor( } // end::SwitchActiveProfileRepository[] - suspend fun removeProfile(profileId: ProfileIdentifier) { + override suspend fun removeProfile(profileId: ProfileIdentifier) { lock.withLock { realm.writeBlocking { val profiles = query().find() @@ -155,7 +162,7 @@ class ProfilesRepository constructor( } } - suspend fun saveInsuranceInformation( + override suspend fun saveInsuranceInformation( profileId: ProfileIdentifier, insurantName: String, insuranceIdentifier: String, @@ -196,7 +203,7 @@ class ProfilesRepository constructor( } } - suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) { + override suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) { realm.write { queryFirst("id = $0", profileId)?.apply { this.name = profileName @@ -204,7 +211,7 @@ class ProfilesRepository constructor( } } - suspend fun updateProfileColor(profileId: ProfileIdentifier, color: ProfilesData.ProfileColorNames) { + override suspend fun updateProfileColor(profileId: ProfileIdentifier, color: ProfilesData.ProfileColorNames) { realm.write { queryFirst("id = $0", profileId)?.apply { this.color = when (color) { @@ -218,7 +225,7 @@ class ProfilesRepository constructor( } } - suspend fun updateLastAuthenticated(profileId: ProfileIdentifier, lastAuthenticated: Instant) { + override suspend fun updateLastAuthenticated(profileId: ProfileIdentifier, lastAuthenticated: Instant) { realm.write { queryFirst("id = $0", profileId)?.apply { this.lastAuthenticated = lastAuthenticated.toRealmInstant() @@ -226,30 +233,30 @@ class ProfilesRepository constructor( } } - suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatarFigure: ProfilesData.AvatarFigure) { + override suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) { realm.write { queryFirst("id = $0", profileId)?.apply { - this.avatarFigure = when (avatarFigure) { - ProfilesData.AvatarFigure.PersonalizedImage -> AvatarFigureV1.PersonalizedImage - ProfilesData.AvatarFigure.FemaleDoctor -> AvatarFigureV1.FemaleDoctor - ProfilesData.AvatarFigure.WomanWithHeadScarf -> AvatarFigureV1.WomanWithHeadScarf - ProfilesData.AvatarFigure.Grandfather -> AvatarFigureV1.Grandfather - ProfilesData.AvatarFigure.BoyWithHealthCard -> AvatarFigureV1.BoyWithHealthCard - ProfilesData.AvatarFigure.OldManOfColor -> AvatarFigureV1.OldManOfColor - ProfilesData.AvatarFigure.WomanWithPhone -> AvatarFigureV1.WomanWithPhone - ProfilesData.AvatarFigure.Grandmother -> AvatarFigureV1.Grandmother - ProfilesData.AvatarFigure.ManWithPhone -> AvatarFigureV1.ManWithPhone - ProfilesData.AvatarFigure.WheelchairUser -> AvatarFigureV1.WheelchairUser - ProfilesData.AvatarFigure.Baby -> AvatarFigureV1.Baby - ProfilesData.AvatarFigure.MaleDoctorWithPhone -> AvatarFigureV1.MaleDoctorWithPhone - ProfilesData.AvatarFigure.FemaleDoctorWithPhone -> AvatarFigureV1.FemaleDoctorWithPhone - ProfilesData.AvatarFigure.FemaleDeveloper -> AvatarFigureV1.FemaleDeveloper + this.avatarFigure = when (avatar) { + ProfilesData.Avatar.PersonalizedImage -> AvatarFigureV1.PersonalizedImage + ProfilesData.Avatar.FemaleDoctor -> AvatarFigureV1.FemaleDoctor + ProfilesData.Avatar.WomanWithHeadScarf -> AvatarFigureV1.WomanWithHeadScarf + ProfilesData.Avatar.Grandfather -> AvatarFigureV1.Grandfather + ProfilesData.Avatar.BoyWithHealthCard -> AvatarFigureV1.BoyWithHealthCard + ProfilesData.Avatar.OldManOfColor -> AvatarFigureV1.OldManOfColor + ProfilesData.Avatar.WomanWithPhone -> AvatarFigureV1.WomanWithPhone + ProfilesData.Avatar.Grandmother -> AvatarFigureV1.Grandmother + ProfilesData.Avatar.ManWithPhone -> AvatarFigureV1.ManWithPhone + ProfilesData.Avatar.WheelchairUser -> AvatarFigureV1.WheelchairUser + ProfilesData.Avatar.Baby -> AvatarFigureV1.Baby + ProfilesData.Avatar.MaleDoctorWithPhone -> AvatarFigureV1.MaleDoctorWithPhone + ProfilesData.Avatar.FemaleDoctorWithPhone -> AvatarFigureV1.FemaleDoctorWithPhone + ProfilesData.Avatar.FemaleDeveloper -> AvatarFigureV1.FemaleDeveloper } } } } - suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) { + override suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) { realm.write { queryFirst("id = $0", profileId)?.apply { this.personalizedImage = profileImage @@ -257,7 +264,7 @@ class ProfilesRepository constructor( } } - suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) { + override suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) { realm.write { queryFirst("id = $0", profileId)?.apply { this.personalizedImage = null @@ -266,7 +273,7 @@ class ProfilesRepository constructor( } } - suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { + override suspend fun switchProfileToPKV(profileId: ProfileIdentifier) { realm.write { queryFirst("id = $0", profileId)?.apply { this.insuranceType = InsuranceTypeV1.PKV diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt new file mode 100644 index 00000000..cbe25dcc --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfileRepository.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.repository + +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import kotlinx.coroutines.flow.Flow +import kotlinx.datetime.Instant + +interface ProfileRepository { + fun profiles(): Flow> + fun activeProfile(): Flow + suspend fun saveProfile(profileName: String, activate: Boolean) + suspend fun activateProfile(profileId: ProfileIdentifier) + suspend fun removeProfile(profileId: ProfileIdentifier) + suspend fun saveInsuranceInformation( + profileId: ProfileIdentifier, + insurantName: String, + insuranceIdentifier: String, + insuranceName: String + ) + suspend fun updateProfileName(profileId: ProfileIdentifier, profileName: String) + suspend fun updateProfileColor(profileId: ProfileIdentifier, color: ProfilesData.ProfileColorNames) + suspend fun updateLastAuthenticated(profileId: ProfileIdentifier, lastAuthenticated: Instant) + suspend fun saveAvatarFigure(profileId: ProfileIdentifier, avatar: ProfilesData.Avatar) + suspend fun savePersonalizedProfileImage(profileId: ProfileIdentifier, profileImage: ByteArray) + suspend fun clearPersonalizedProfileImage(profileId: ProfileIdentifier) + suspend fun switchProfileToPKV(profileId: ProfileIdentifier) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt new file mode 100644 index 00000000..7eadd76e --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/PairedDevice.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase.model + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlinx.datetime.Instant + +@Immutable +data class PairedDevice( + val name: String, + val alias: String, + val connectedOn: Instant +) { + @Stable + fun isOurDevice(alias: String) = this.alias == alias +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt new file mode 100644 index 00000000..5315e62c --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfileInsuranceInformation.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase.model + +data class ProfileInsuranceInformation( + val insurantName: String = "", + val insuranceIdentifier: String = "", + val insuranceName: String = "", + val insuranceType: ProfilesUseCaseData.InsuranceType = ProfilesUseCaseData.InsuranceType.NONE +) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt new file mode 100644 index 00000000..382322fd --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/profiles/usecase/model/ProfilesUseCaseData.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.profiles.usecase.model + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import de.gematik.ti.erp.app.idp.model.IdpData +import de.gematik.ti.erp.app.profiles.model.ProfilesData +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +object ProfilesUseCaseData { + + enum class InsuranceType { + GKV, + PKV, + NONE + } + + @Immutable + data class Profile( + val id: ProfileIdentifier, + val name: String, + val insurance: ProfileInsuranceInformation, + val active: Boolean, + val color: ProfilesData.ProfileColorNames, + val avatar: ProfilesData.Avatar, + val image: ByteArray? = null, + val lastAuthenticated: Instant? = null, + val ssoTokenScope: IdpData.SingleSignOnTokenScope? + ) { + fun ssoTokenValid(now: Instant = Clock.System.now()) = ssoTokenScope?.token?.isValid(now) ?: false + fun hasNoImageSelected() = this.avatar == ProfilesData.Avatar.PersonalizedImage && + this.image == null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Profile + + if (id != other.id) return false + if (name != other.name) return false + if (insurance != other.insurance) return false + if (active != other.active) return false + if (color != other.color) return false + if (avatar != other.avatar) return false + if (image != null) { + if (other.image == null) return false + if (!image.contentEquals(other.image)) return false + } else if (other.image != null) return false + if (lastAuthenticated != other.lastAuthenticated) return false + if (ssoTokenScope != other.ssoTokenScope) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + insurance.hashCode() + result = 31 * result + active.hashCode() + result = 31 * result + color.hashCode() + result = 31 * result + avatar.hashCode() + result = 31 * result + (image?.contentHashCode() ?: 0) + result = 31 * result + (lastAuthenticated?.hashCode() ?: 0) + result = 31 * result + (ssoTokenScope?.hashCode() ?: 0) + return result + } + + companion object { + + enum class ProfileConnectionState { + LoggedIn, + LoggedOutWithoutTokenBiometrics, + LoggedOutWithoutToken, + LoggedOut, + NeverConnected + } + + // old state: ssoTokenScope == null && lastAuthenticated == null + private fun Profile.neverConnected() = lastAuthenticated == null + + private fun Profile.ssoTokenSetAndConnected() = + ssoTokenScope?.token != null && ssoTokenScope.token?.isValid() == true + + private fun Profile.ssoTokenSetAndDisconnected() = + ssoTokenScope != null && ssoTokenScope.token?.isValid() == false || + lastAuthenticated != null + + private fun Profile.ssoTokenNotSet() = + when (ssoTokenScope) { + is IdpData.ExternalAuthenticationToken, + is IdpData.AlternateAuthenticationToken, + is IdpData.AlternateAuthenticationWithoutToken, + is IdpData.DefaultToken -> ssoTokenScope.token == null + + null -> true + } + + private fun Profile.ssoTokenWithoutScope() = + when (ssoTokenScope) { + is IdpData.AlternateAuthenticationWithoutToken -> true + else -> false + } + fun List.activeProfile() = first { it.active } + fun List.profileById(id: ProfileIdentifier?) = firstOrNull { it.id == id } + fun List.containsProfileWithName(name: String) = any { it.name == name.trim() } + + @Stable + fun Profile.connectionState(): ProfileConnectionState? = + when { + this.neverConnected() -> ProfileConnectionState.NeverConnected + + this.ssoTokenWithoutScope() -> ProfileConnectionState.LoggedOutWithoutTokenBiometrics + + this.ssoTokenNotSet() -> ProfileConnectionState.LoggedOutWithoutToken + + this.ssoTokenSetAndConnected() -> ProfileConnectionState.LoggedIn + + this.ssoTokenSetAndDisconnected() -> ProfileConnectionState.LoggedOut + + else -> null + } + } + } + + @Immutable + data class PairedDevices( + val devices: List + ) +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt index ee17135f..ece21ca4 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/ProtocolModule.kt @@ -20,13 +20,17 @@ package de.gematik.ti.erp.app.protocol import de.gematik.ti.erp.app.protocol.repository.AuditEventRemoteDataSource import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository +import de.gematik.ti.erp.app.protocol.repository.DefaultAuditEventsRepository import de.gematik.ti.erp.app.protocol.usecase.AuditEventsUseCase import org.kodein.di.DI import org.kodein.di.bindProvider import org.kodein.di.instance val protocolModule = DI.Module("protocolModule") { - bindProvider { AuditEventsRepository(instance()) } bindProvider { AuditEventRemoteDataSource(instance()) } bindProvider { AuditEventsUseCase(instance(), instance()) } } + +val protocolRepositoryModule = DI.Module("protocolRepositoryModule", allowSilentOverride = true) { + bindProvider { DefaultAuditEventsRepository(instance()) } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt index 47894b52..31531738 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepository.kt @@ -18,97 +18,14 @@ package de.gematik.ti.erp.app.protocol.repository -import de.gematik.ti.erp.app.fhir.model.mapCatching -import de.gematik.ti.erp.app.fhir.parser.contained -import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull -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.stringValue import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier import de.gematik.ti.erp.app.protocol.model.AuditEventData -import de.gematik.ti.erp.app.utils.asFhirInstant -import io.github.aakira.napier.Napier -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonPrimitive -/** - * - */ -class AuditEventsRepository( - private val remoteDataSource: AuditEventRemoteDataSource -) { +interface AuditEventsRepository { suspend fun downloadAuditEvents( profileId: ProfileIdentifier, count: Int?, offset: Int? - ) = - remoteDataSource.getAuditEvents( - profileId = profileId, - count = count, - offset = offset - ).map { - extractAuditEvents( - bundle = it, - onError = { element, cause -> - Napier.e(cause) { - element.toString() - } - } - ) - } - - private fun extractAuditEvents( - bundle: JsonElement, - onError: (JsonElement, Exception) -> Unit = { _, _ -> } - ): AuditEventData.AuditEventMappingResult { - val bundleTotal = bundle.containedArrayOrNull("entry")?.size ?: 0 - val bundleId = bundle.containedString("id") - val resources = bundle - .findAll(listOf("entry", "resource")) - - val auditEvents = resources.mapCatching(onError) { resource -> - val id = resource.containedString("id") - val text = resource.contained("text").containedString("div") - val taskId = resource - .findAll(listOf("entity", "what", "identifier")) - .filterWith( - "system", - stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID") - ) - .firstOrNull() - ?.containedString("value") - ?: resource - .findAll(listOf("entity", "what", "identifier")) - .filterWith( - "system", - stringValue("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId") - ) - .firstOrNull() - ?.containedString("value") - - val timestamp = requireNotNull(resource.contained("recorded").jsonPrimitive.asFhirInstant()) { - "Audit event field `recorded` missing" - } - - val description = text.removeSurrounding( - "
", - "
" - ) - - AuditEventData.AuditEvent( - auditId = id, - taskId = taskId, - description = description, - timestamp = timestamp.value - ) - } - - return AuditEventData.AuditEventMappingResult( - auditEvents = auditEvents.toList(), - bundleId = bundleId, - bundleResultCount = bundleTotal - ) - } + ): Result } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt new file mode 100644 index 00000000..63b8a3e4 --- /dev/null +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepository.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +package de.gematik.ti.erp.app.protocol.repository + +import de.gematik.ti.erp.app.fhir.model.mapCatching +import de.gematik.ti.erp.app.fhir.parser.contained +import de.gematik.ti.erp.app.fhir.parser.containedArrayOrNull +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.stringValue +import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier +import de.gematik.ti.erp.app.protocol.model.AuditEventData +import de.gematik.ti.erp.app.utils.asFhirInstant +import io.github.aakira.napier.Napier +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive + +class DefaultAuditEventsRepository( + private val remoteDataSource: AuditEventRemoteDataSource +) : AuditEventsRepository { + + override suspend fun downloadAuditEvents( + profileId: ProfileIdentifier, + count: Int?, + offset: Int? + ) = + remoteDataSource.getAuditEvents( + profileId = profileId, + count = count, + offset = offset + ).map { + extractAuditEvents( + bundle = it, + onError = { element, cause -> + Napier.e(cause) { + element.toString() + } + } + ) + } + + private fun extractAuditEvents( + bundle: JsonElement, + onError: (JsonElement, Exception) -> Unit = { _, _ -> } + ): AuditEventData.AuditEventMappingResult { + val bundleTotal = bundle.containedArrayOrNull("entry")?.size ?: 0 + val bundleId = bundle.containedString("id") + val resources = bundle + .findAll(listOf("entry", "resource")) + + val auditEvents = resources.mapCatching(onError) { resource -> + val id = resource.containedString("id") + val text = resource.contained("text").containedString("div") + val taskId = resource + .findAll(listOf("entity", "what", "identifier")) + .filterWith( + "system", + stringValue("https://gematik.de/fhir/NamingSystem/PrescriptionID") + ) + .firstOrNull() + ?.containedString("value") + ?: resource + .findAll(listOf("entity", "what", "identifier")) + .filterWith( + "system", + stringValue("https://gematik.de/fhir/erp/NamingSystem/GEM_ERP_NS_PrescriptionId") + ) + .firstOrNull() + ?.containedString("value") + + val timestamp = requireNotNull(resource.contained("recorded").jsonPrimitive.asFhirInstant()) { + "Audit event field `recorded` missing" + } + + val description = text.removeSurrounding( + "
", + "
" + ) + + AuditEventData.AuditEvent( + auditId = id, + taskId = taskId, + description = description, + timestamp = timestamp.value + ) + } + + return AuditEventData.AuditEventMappingResult( + auditEvents = auditEvents.toList(), + bundleId = bundleId, + bundleResultCount = bundleTotal + ) + } +} diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt index 5e6443aa..51949d92 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/protocol/usecase/AuditEventsUseCase.kt @@ -25,12 +25,10 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import de.gematik.ti.erp.app.DispatchProvider import de.gematik.ti.erp.app.profiles.repository.ProfileIdentifier - import de.gematik.ti.erp.app.protocol.model.AuditEventData import de.gematik.ti.erp.app.protocol.repository.AuditEventsRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn - import kotlinx.coroutines.withContext import kotlin.math.max @@ -52,10 +50,10 @@ class AuditEventsUseCase( maxSize = AuditEventsInitialResultsPerPage * 2 ), pagingSourceFactory = { AuditEventPagingSource(profileId) } - ).flow.flowOn(dispatchers.IO) + ).flow.flowOn(dispatchers.io) } suspend fun loadAuditEvents(profileId: ProfileIdentifier): List = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val initialResult = auditRepository.downloadAuditEvents( profileId, null, 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 a4756d55..bb785863 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 @@ -69,8 +69,9 @@ class SettingsRepository constructor( screenShotsAllowed = it.screenshotsAllowed ) } - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) + // TODO: Not used override val authenticationMode: Flow get() = realm.query().first().asFlow().mapNotNull { query -> query.obj?.let { @@ -87,9 +88,10 @@ class SettingsRepository constructor( else -> SettingsData.AuthenticationMode.Unspecified } } - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) - override val pharmacySearch: Flow // TODO move to PharmacySearch + // TODO: Not used + override val pharmacySearch: Flow get() = settings.mapNotNull { settings -> settings?.pharmacySearch?.let { SettingsData.PharmacySearch( @@ -100,8 +102,9 @@ class SettingsRepository constructor( openNow = it.filterOpenNow ) } - }.flowOn(dispatchers.IO) + }.flowOn(dispatchers.io) + // TODO move to PharmacySearch override suspend fun savePharmacySearch(search: SettingsData.PharmacySearch) { writeToRealm { this.pharmacySearch?.apply { @@ -148,7 +151,7 @@ class SettingsRepository constructor( profileName: String, now: Instant ) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { realm.writeToRealm { settings -> copyToRealm( ProfileEntityV1().apply { @@ -221,7 +224,7 @@ class SettingsRepository constructor( } private suspend fun writeToRealm(block: SettingsEntityV1.() -> Unit) { - withContext(dispatchers.IO) { + withContext(dispatchers.io) { realm.writeToRealm { it.block() } diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt index 0769d961..2c87067d 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/utils/TemporalConverter.kt @@ -20,8 +20,6 @@ package de.gematik.ti.erp.app.utils import de.gematik.ti.erp.app.fhir.parser.Year import de.gematik.ti.erp.app.fhir.parser.YearMonth -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.contentOrNull import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -31,9 +29,10 @@ import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toInstant import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.contentOrNull import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -import kotlin.jvm.JvmInline /** * The Fhir documentation mentions the following formats: @@ -94,6 +93,14 @@ sealed interface FhirTemporal { .toFormattedDate() } +fun Instant.asFhirTemporal(): FhirTemporal.Instant { + val desiredFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + val updatedInstant = toLocalDateTime(TimeZone.currentSystemDefault()) + .toJavaLocalDateTime().format(desiredFormatter) + .toInstant() + return FhirTemporal.Instant(updatedInstant) +} + fun Instant.toFormattedDateTime(): String? = this.toLocalDateTime(TimeZone.currentSystemDefault()) .toJavaLocalDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT)) fun Instant.toStartOfDayInUTC(): Instant { @@ -102,7 +109,6 @@ fun Instant.toStartOfDayInUTC(): Instant { } fun Instant.toFormattedDate(): String? = this.toLocalDateTime(TimeZone.currentSystemDefault()) .toJavaLocalDateTime().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)) -fun Instant.asFhirTemporal() = FhirTemporal.Instant(this) fun LocalDateTime.asFhirTemporal() = FhirTemporal.LocalDateTime(this) fun LocalDate.asFhirTemporal() = FhirTemporal.LocalDate(this) fun YearMonth.asFhirTemporal() = FhirTemporal.YearMonth(this) diff --git a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt index 9769e161..3c5f01e7 100644 --- a/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt +++ b/common/src/commonMain/kotlin/de/gematik/ti/erp/app/vau/repository/VauRepository.kt @@ -35,7 +35,7 @@ class VauRepository( * rethrows the exception. */ suspend fun withUntrusted(block: suspend (UntrustedCertList, UntrustedOCSPList) -> R) = - withContext(dispatchers.IO) { + withContext(dispatchers.io) { val (untrustedCertList, untrustedOCSPList) = localDataSource.loadUntrusted() ?: run { Napier.d("GET cert & ocsp from backend...") diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt index eb3baabf..38738a7c 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/CoroutineTestRule.kt @@ -35,10 +35,10 @@ class CoroutineTestRule( ) : TestWatcher() { val dispatchers = object : DispatchProvider { - override val Default: CoroutineDispatcher get() = testDispatcher - override val IO: CoroutineDispatcher get() = testDispatcher - override val Main: CoroutineDispatcher get() = testDispatcher - override val Unconfined: CoroutineDispatcher get() = testDispatcher + override val default: CoroutineDispatcher get() = testDispatcher + override val io: CoroutineDispatcher get() = testDispatcher + override val main: CoroutineDispatcher get() = testDispatcher + override val unconfined: CoroutineDispatcher get() = testDispatcher } override fun starting(description: Description?) { diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt index e5bb19c3..891fe82d 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/CommunicationMapperTest.kt @@ -20,14 +20,13 @@ package de.gematik.ti.erp.app.fhir.model -import de.gematik.ti.erp.app.utils.FhirTemporal -import de.gematik.ti.erp.app.utils.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.utils.FhirTemporal import kotlinx.datetime.Instant import kotlinx.serialization.json.Json +import org.junit.Test import java.io.File -import kotlin.test.Test import kotlin.test.assertEquals private const val JsonSymbols = "\"{}[]:" @@ -167,7 +166,7 @@ class CommunicationMapperTest { assertEquals(com.communicationId, communicationId) assertEquals(com.orderId, orderId) assertEquals(com.profile, profile) - assertEquals(com.sentOn.asFhirTemporal(), sentOn) + assertEquals(sentOn.toInstant(), com.sentOn) assertEquals(com.sender, sender) assertEquals(com.recipient, recipient) assertEquals(com.payload, payload) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt index 1f915baa..7bd898a7 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/MedicationDispenseMapperTest.kt @@ -219,7 +219,7 @@ class MedicationDispenseMapperTest { assertEquals("06491772", uniqueIdentifier) assertEquals(listOf(), ingredients) assertEquals("8521037577", lotNumber) - assertEquals(Instant.parse("2023-05-02T06:26:06Z").asFhirTemporal(), expirationDate) + assertEquals(Instant.parse("2023-05-02T06:26:06Z"), expirationDate?.toInstant()) ReturnType.Medication }, processMedicationDispense = { dispenseId, patientIdentifier, medication, wasSubstituted, diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt index ad1c5b69..16444895 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/fhir/model/TaskMapperTest.kt @@ -35,10 +35,10 @@ class TaskMapperTest { process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> assertEquals("160.000.000.029.982.30", taskId) assertEquals("dd23212d35d14ccde351f9a1077f3d9508dcb8629882627ec16a22ea86144290", accessCode) - assertEquals(Instant.parse("2022-06-09T11:57:37.923Z").asFhirTemporal(), lastModified) + assertEquals(Instant.parse("2022-06-09T11:57:37.923Z"), lastModified.toInstant()) assertEquals(LocalDate.parse("2022-09-09").asFhirTemporal(), expiresOn) assertEquals(LocalDate.parse("2022-07-07").asFhirTemporal(), acceptUntil) - assertEquals(Instant.parse("2022-06-09T11:50:23.223Z").asFhirTemporal(), authoredOn) + assertEquals(Instant.parse("2022-06-09T11:50:23.223Z"), authoredOn.toInstant()) assertEquals(TaskStatus.Completed, status) } ) @@ -52,10 +52,10 @@ class TaskMapperTest { process = { taskId, accessCode, lastModified, expiresOn, acceptUntil, authoredOn, status -> assertEquals("160.000.033.491.280.78", taskId) assertEquals("777bea0e13cc9c42ceec14aec3ddee2263325dc2c6c699db115f58fe423607ea", accessCode) - assertEquals(Instant.parse("2022-03-18T15:29:00Z").asFhirTemporal(), lastModified) + assertEquals(Instant.parse("2022-03-18T15:29:00Z"), lastModified.toInstant()) assertEquals(LocalDate.parse("2022-06-02").asFhirTemporal(), expiresOn) assertEquals(LocalDate.parse("2022-04-02").asFhirTemporal(), acceptUntil) - assertEquals(Instant.parse("2022-03-18T15:26:00Z").asFhirTemporal(), authoredOn) + assertEquals(Instant.parse("2022-03-18T15:26:00Z"), authoredOn.toInstant()) assertEquals(TaskStatus.Completed, status) } ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt index 0d9e45ba..96327120 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/repository/IdpRepositoryTest.kt @@ -52,7 +52,8 @@ import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 import de.gematik.ti.erp.app.fhir.model.ResourceBasePath import de.gematik.ti.erp.app.idp.EllipticCurvesExtending import de.gematik.ti.erp.app.idp.model.IdpData -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -124,7 +125,7 @@ class CommonIdpRepositoryTest : TestDB() { lateinit var remoteDataSource: IdpRemoteDataSource lateinit var idpLocalDataSource: IdpLocalDataSource - lateinit var profileRepository: ProfilesRepository + lateinit var profileRepository: ProfileRepository @BeforeTest fun setUp() { @@ -173,7 +174,7 @@ class CommonIdpRepositoryTest : TestDB() { localDataSource = idpLocalDataSource ) - profileRepository = ProfilesRepository( + profileRepository = DefaultProfilesRepository( dispatchers = coroutineRule.dispatchers, realm = realm ) diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt index a90661ac..e4b2adb9 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/idp/usecase/IdpBasicUseCaseTest.kt @@ -29,16 +29,14 @@ import io.mockk.coVerifyOrder import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import kotlinx.datetime.Clock import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test +import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.hours -@OptIn(ExperimentalCoroutinesApi::class) class IdpBasicUseCaseTest { @get:Rule val coroutineRule = CoroutineTestRule() 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 a1a2b0b7..d01dacd7 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 @@ -29,7 +29,7 @@ import de.gematik.ti.erp.app.idp.repository.IdpLocalDataSource import de.gematik.ti.erp.app.idp.repository.IdpPairingRepository import de.gematik.ti.erp.app.idp.repository.IdpRemoteDataSource import de.gematik.ti.erp.app.idp.repository.IdpRepository -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository import de.gematik.ti.erp.app.vau.usecase.TruststoreUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -69,7 +69,7 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class IdpIntegrationTest { @MockK(relaxed = true) - private lateinit var profilesRepository: ProfilesRepository + private lateinit var profilesRepository: DefaultProfilesRepository @MockK private lateinit var truststoreUseCase: TruststoreUseCase @@ -143,7 +143,7 @@ class IdpIntegrationTest { truststoreUseCase = truststoreUseCase ) - useCase = IdpUseCase( + useCase = DefaultIdpUseCase( repository = idpRepository, pairingRepository = idpPairingRepository, altAuthUseCase = IdpAlternateAuthenticationUseCase( diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt index e6b74a0a..3883ada4 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/invoice/repository/InvoiceRepositoryTest.kt @@ -49,7 +49,8 @@ import de.gematik.ti.erp.app.db.entities.v1.task.RatioEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.ScannedTaskEntityV1 import de.gematik.ti.erp.app.db.entities.v1.task.SyncedTaskEntityV1 import de.gematik.ti.erp.app.fhir.model.chargeItem_freetext -import de.gematik.ti.erp.app.profiles.repository.ProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.DefaultProfilesRepository +import de.gematik.ti.erp.app.profiles.repository.ProfileRepository import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import io.realm.kotlin.Realm @@ -70,7 +71,7 @@ class InvoiceRepositoryTest : TestDB() { lateinit var invoiceLocalDataSource: InvoiceLocalDataSource lateinit var invoiceRemoteDataSource: InvoiceRemoteDataSource lateinit var invoiceRepository: InvoiceRepository - lateinit var profileRepository: ProfilesRepository + lateinit var profileRepository: ProfileRepository lateinit var realm: Realm @@ -124,7 +125,7 @@ class InvoiceRepositoryTest : TestDB() { invoiceLocalDataSource, coroutineRule.dispatchers ) - profileRepository = ProfilesRepository(coroutineRule.dispatchers, realm) + profileRepository = DefaultProfilesRepository(coroutineRule.dispatchers, realm) } @Test diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt index b5be16e9..69c1f11a 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/profiles/repository/ProfilesRepositoryTest.kt @@ -74,7 +74,7 @@ class ProfilesRepositoryTest : TestDB() { lateinit var realm: Realm - lateinit var repo: ProfilesRepository + lateinit var repo: DefaultProfilesRepository @BeforeTest fun setUp() { @@ -114,7 +114,7 @@ class ProfilesRepositoryTest : TestDB() { .build() ) - repo = ProfilesRepository( + repo = DefaultProfilesRepository( dispatchers = coroutineRule.dispatchers, realm = realm ) @@ -315,12 +315,12 @@ class ProfilesRepositoryTest : TestDB() { @Test fun `save avatar figure`() = runTest { repo.saveProfile(defaultProfileName, true) - ProfilesData.AvatarFigure.values().forEach { figure -> + ProfilesData.Avatar.values().forEach { figure -> repo.profiles().first().also { repo.saveAvatarFigure(it[0].id, figure) } repo.profiles().first().also { - assertEquals(figure, it[0].avatarFigure) + assertEquals(figure, it[0].avatar) } } } diff --git a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepositoryTest.kt b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt similarity index 96% rename from common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepositoryTest.kt rename to common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt index 3095aa33..b9021e70 100644 --- a/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/AuditEventsRepositoryTest.kt +++ b/common/src/commonTest/kotlin/de/gematik/ti/erp/app/protocol/repository/DefaultAuditEventsRepositoryTest.kt @@ -42,11 +42,11 @@ private val testAuditEventVersion12 by lazy { @get:Rule val coroutineRule = CoroutineTestRule() -class AuditEventsRepositoryTest { +class DefaultAuditEventsRepositoryTest { @MockK lateinit var remoteDataSource: AuditEventRemoteDataSource - private lateinit var auditEventsRepository: AuditEventsRepository + private lateinit var auditEventsRepository: DefaultAuditEventsRepository private class AuditEvent( val id: String, val taskId: String?, @@ -88,7 +88,7 @@ class AuditEventsRepositoryTest { @BeforeTest fun setUp() { MockKAnnotations.init(this) - auditEventsRepository = AuditEventsRepository(remoteDataSource) + auditEventsRepository = DefaultAuditEventsRepository(remoteDataSource) } @Test diff --git a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt index 6548559a..a5e7b184 100644 --- a/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt +++ b/common/src/desktopMain/kotlin/de/gematik/ti/erp/app/vau/interceptor/VauChannelInterceptor.kt @@ -56,7 +56,7 @@ class VauChannelInterceptor( override fun intercept(chain: Interceptor.Chain): Response { try { - val encryptedRequest = runBlocking(dispatchProvider.IO) { + val encryptedRequest = runBlocking(dispatchProvider.io) { truststore.withValidVauPublicKey { publicKey -> VauChannelSpec.V1.encryptHttpRequest( chain.request(), diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index f76f9707..b5c7cbe6 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -118,8 +118,8 @@ complexity: threshold: 120 LongParameterList: active: true - functionThreshold: 12 - constructorThreshold: 7 + functionThreshold: 15 + constructorThreshold: 15 ignoreDefaultParameters: true ignoreDataClasses: true ignoreAnnotatedParameter: [] @@ -150,7 +150,7 @@ complexity: thresholdInObjects: 15 thresholdInEnums: 15 ignoreDeprecated: false - ignorePrivate: false + ignorePrivate: true ignoreOverridden: false coroutines: @@ -554,7 +554,7 @@ style: MandatoryBracesLoops: active: true MaxLineLength: - active: true + active: false maxLineLength: 120 excludePackageStatements: true excludeImportStatements: true diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index fe6a976d..ad0df706 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -1,7 +1,7 @@ -import de.gematik.ti.erp.app +import de.gematik.ti.erp.Dependencies +import de.gematik.ti.erp.inject import de.gematik.ti.erp.networkSecurityConfigGen.AndroidNetworkConfigGeneratorTask import de.gematik.ti.erp.stringResGen.AndroidStringResourceGeneratorTask -import org.jetbrains.compose.compose import org.jetbrains.compose.desktop.application.dsl.TargetFormat import java.util.Locale @@ -58,7 +58,7 @@ fun networkConfigPath(name: String): String { kotlin { jvm { compilations.all { - kotlinOptions.jvmTarget = "17" + kotlinOptions.jvmTarget = Dependencies.Versions.JavaVersion.KOTLIN_OPTIONS_JVM_TARGET kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } withJava() @@ -76,16 +76,18 @@ kotlin { implementation(compose.materialIconsExtended) - app { + inject { androidX { - compileOnly(paging("common-ktx")) + compileOnly(multiplatformPaging) } - kotlinX { - implementation(coroutines("swing")) + coroutines { + implementation(coroutinesSwing) + } + dateTime { implementation(datetime) } dependencyInjection { - compileOnly(kodein("di-framework-compose")) + compileOnly(kodeinCompose) } dataMatrix { implementation(zxing) @@ -100,14 +102,14 @@ kotlin { } crypto { implementation(jose4j) - implementation(bouncyCastle("bcprov")) - implementation(bouncyCastle("bcpkix")) + implementation(bouncycastleBcprov) + implementation(bouncycastleBcpkix) } network { - implementation(retrofit2("retrofit")) + implementation(retrofit) implementation(retrofit2KotlinXSerialization) - implementation(okhttp3("okhttp")) - implementation(okhttp3("logging-interceptor")) + implementation(okhttp3) + implementation(okhttpLogging) // Work around vulnerable Okio version 3.1.0 (CVE-2023-3635). // Can be removed when Retrofit releases a new version >2.9.0. implementation(okio) @@ -132,19 +134,23 @@ compose.desktop { modules("java.smartcardio") macOS { - // iconFile.set(rootProject.file("resources/icon/ERezept.icns")) + iconFile.set(rootProject.file("resources/icon/ERezept.icns")) } windows { - val path = if ((project.property("buildkonfig.flavor") as String).endsWith("Internal")) { - "E-Rezept-Dev.ico" - } else { - "E-Rezept.ico" - } - iconFile.set(project.file(path)) + iconFile.set( + project.file( + when { + (project.property("buildkonfig.flavor") as? String) + ?.endsWith("Internal") == true -> "E-Rezept-Dev.ico" + + else -> "E-Rezept.ico" + } + ) + ) menuGroup = "gematik" } linux { - // iconFile.set(rootProject.file("resources/icon/ERezept.png")) + iconFile.set(rootProject.file("resources/icon/ERezept.png")) } } } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt index 842609e0..fcb7d2ab 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/DownloadUseCase.kt @@ -18,15 +18,15 @@ package de.gematik.ti.erp.app -import de.gematik.ti.erp.app.communication.repository.CommunicationRepository -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository +import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope class DownloadUseCase( - private val prescriptionRepository: PrescriptionRepository, - private val communicationRepository: CommunicationRepository + private val prescriptionRepository: DesktopPrescriptionRepository, + private val communicationRepository: DesktopCommunicationRepository ) { suspend fun update() = supervisorScope { diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt index 21292178..da889355 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/di/CommunicationModule.kt @@ -18,7 +18,7 @@ package de.gematik.ti.erp.app.communication.di -import de.gematik.ti.erp.app.communication.repository.CommunicationRepository +import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository import de.gematik.ti.erp.app.communication.repository.LocalDataSource import de.gematik.ti.erp.app.communication.repository.RemoteDataSource import de.gematik.ti.erp.app.communication.usecase.CommunicationUseCase @@ -32,6 +32,6 @@ import org.kodein.di.singleton fun communicationModule(scope: Scope) = DI.Module("Communication Module") { bind { scoped(scope).singleton { RemoteDataSource(instance()) } } bind { scoped(scope).singleton { LocalDataSource() } } - bind { scoped(scope).singleton { CommunicationRepository(instance(), instance(), instance()) } } + bind { scoped(scope).singleton { DesktopCommunicationRepository(instance(), instance(), instance()) } } bind { scoped(scope).singleton { CommunicationUseCase(instance(), instance()) } } } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/CommunicationRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt similarity index 97% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/CommunicationRepository.kt rename to desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt index fa156795..e60972f5 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/CommunicationRepository.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/repository/DesktopCommunicationRepository.kt @@ -20,7 +20,7 @@ package de.gematik.ti.erp.app.communication.repository import de.gematik.ti.erp.app.fhir.FhirMapper -class CommunicationRepository( +class DesktopCommunicationRepository( private val localDataSource: LocalDataSource, private val remoteDataSource: RemoteDataSource, private val mapper: FhirMapper diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt index 089b9530..e7222863 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/ui/CommunicationViewModel.kt @@ -37,5 +37,5 @@ class CommunicationViewModel( fun screenState(): Flow = communicationUseCase.pharmacyCommunications().map { CommunicationScreenData.State(it) - }.flowOn(dispatchProvider.Unconfined) + }.flowOn(dispatchProvider.unconfined) } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt index a2f16ad0..497d515c 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/communication/usecase/CommunicationUseCase.kt @@ -18,20 +18,20 @@ package de.gematik.ti.erp.app.communication.usecase -import de.gematik.ti.erp.app.communication.repository.CommunicationRepository +import de.gematik.ti.erp.app.communication.repository.DesktopCommunicationRepository import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData import de.gematik.ti.erp.app.communication.usecase.model.CommunicationUseCaseData.Communication.SupplyOption import de.gematik.ti.erp.app.prescription.repository.CommunicationProfile.Reply import de.gematik.ti.erp.app.prescription.repository.CommunicationSupplyOption -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository import de.gematik.ti.erp.app.prescription.repository.SimpleCommunicationWithPharmacy import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class CommunicationUseCase( - private val communicationRepository: CommunicationRepository, - private val prescriptionRepository: PrescriptionRepository + private val communicationRepository: DesktopCommunicationRepository, + private val prescriptionRepository: DesktopPrescriptionRepository ) { fun pharmacyCommunications(): Flow> = communicationRepository.communications().map { diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt index 51f04452..a80020fc 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/login/ui/LoginWithHealthCardViewModel.kt @@ -91,7 +91,7 @@ class LoginWithHealthCardViewModel( emit(state) } } - .flowOn(dispatchProvider.IO) + .flowOn(dispatchProvider.io) override fun close() { executor.shutdownNow() diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt index b74b587d..b5cd6d81 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/di/PrescriptionModule.kt @@ -18,9 +18,9 @@ package de.gematik.ti.erp.app.prescription.di -import de.gematik.ti.erp.app.prescription.repository.LocalDataSource -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository -import de.gematik.ti.erp.app.prescription.repository.RemoteDataSource +import de.gematik.ti.erp.app.prescription.repository.PrescriptionLocalDataSource +import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.PrescriptionRemoteDataSource import de.gematik.ti.erp.app.prescription.usecase.PrescriptionMapper import de.gematik.ti.erp.app.prescription.usecase.PrescriptionUseCase import org.kodein.di.DI @@ -32,9 +32,9 @@ import org.kodein.di.scoped import org.kodein.di.singleton fun prescriptionModule(scope: Scope) = DI.Module("Prescription Module") { - bind { scoped(scope).singleton { RemoteDataSource(instance()) } } - bind { scoped(scope).singleton { LocalDataSource() } } - bind { scoped(scope).singleton { PrescriptionRepository(instance(), instance(), instance(), instance()) } } + bind { scoped(scope).singleton { PrescriptionRemoteDataSource(instance()) } } + bind { scoped(scope).singleton { PrescriptionLocalDataSource() } } + bind { scoped(scope).singleton { DesktopPrescriptionRepository(instance(), instance(), instance(), instance()) } } bind { scoped(scope).singleton { PrescriptionUseCase(instance(), instance()) } } bindInstance { PrescriptionMapper() } } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt similarity index 93% rename from desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt rename to desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt index 5e7b8c6a..846d720f 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/PrescriptionRepository.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/repository/DesktopPrescriptionRepository.kt @@ -25,10 +25,10 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext -class PrescriptionRepository( +class DesktopPrescriptionRepository( private val dispatchProvider: DispatchProvider, - private val localDataSource: LocalDataSource, - private val remoteDataSource: RemoteDataSource, + private val localDataSource: PrescriptionLocalDataSource, + private val remoteDataSource: PrescriptionRemoteDataSource, private val mapper: FhirMapper ) { fun tasks() = localDataSource.loadTasks() @@ -38,7 +38,7 @@ class PrescriptionRepository( suspend fun download(): Result = loadTasksRemote().mapCatching { taskIds -> supervisorScope { - withContext(dispatchProvider.IO) { + withContext(dispatchProvider.io) { taskIds.map { taskId -> async { downloadTaskWithKBVBundle(taskId) } } + async { diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt index e9f7826d..ac6b8937 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionDetailScreen.kt @@ -18,7 +18,6 @@ package de.gematik.ti.erp.app.prescription.ui -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.layout.Arrangement @@ -81,7 +80,6 @@ import java.util.Locale private const val missingValue = "---" -@OptIn(ExperimentalAnimationApi::class) @Composable fun PrescriptionDetailsScreen( navigation: Navigation, diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt index 884ea2dc..96c3ccc2 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/ui/PrescriptionViewModel.kt @@ -42,7 +42,7 @@ class PrescriptionViewModel( private val dispatchProvider: DispatchProvider, private val prescriptionUseCase: PrescriptionUseCase ) : ScopeCloseable { - private val deleteScope = CoroutineScope(dispatchProvider.IO) + private val deleteScope = CoroutineScope(dispatchProvider.io) private val deleteResult = MutableSharedFlow>() private val selectedPrescription = MutableSharedFlow() private val prescriptionType = MutableStateFlow(PrescriptionUseCase.PrescriptionType.NotDispensed) @@ -106,7 +106,7 @@ class PrescriptionViewModel( prescriptionType.emit(PrescriptionUseCase.PrescriptionType.NotDispensed) } - suspend fun update() = withContext(dispatchProvider.IO) { + suspend fun update() = withContext(dispatchProvider.io) { prescriptionUseCase.update() } diff --git a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt index 2833ca2d..b6f0537a 100644 --- a/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt +++ b/desktop/src/jvmMain/kotlin/de/gematik/ti/erp/app/prescription/usecase/PrescriptionUseCase.kt @@ -21,7 +21,7 @@ package de.gematik.ti.erp.app.prescription.usecase import com.google.zxing.BarcodeFormat import com.google.zxing.common.BitMatrix import com.google.zxing.datamatrix.DataMatrixWriter -import de.gematik.ti.erp.app.prescription.repository.PrescriptionRepository +import de.gematik.ti.erp.app.prescription.repository.DesktopPrescriptionRepository import de.gematik.ti.erp.app.prescription.repository.model.SimpleAuditEvent import de.gematik.ti.erp.app.prescription.repository.model.SimpleMedicationDispense import de.gematik.ti.erp.app.prescription.repository.model.SimpleTask @@ -35,7 +35,7 @@ import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.putJsonArray class PrescriptionUseCase( - private val repository: PrescriptionRepository, + private val repository: DesktopPrescriptionRepository, private val mapper: PrescriptionMapper ) { enum class PrescriptionType { diff --git a/gradle.properties b/gradle.properties index 5c74efd6..5a2492c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,9 +26,9 @@ buildkonfig.flavor=googleTuInternal # VERSION_CODE=1 VERSION_NAME=1.0 -USER_AGENT=eRp-App-Android/1.16.1 GMTIK/eRezeptApp +USER_AGENT=eRp-App-Android/1.17.0 GMTIK/eRezeptApp # -DATA_PROTECTION_LAST_UPDATED = 2022-01-06 +DATA_PROTECTION_LAST_UPDATED=2022-01-06 # # orchestrator; can be de.gematik.ti.erp.app.test.test.CucumberTest TEST_INSTRUMENTATION_ORCHESTRATOR=androidx.test.runner.AndroidJUnitRunner @@ -52,4 +52,8 @@ APP_TRUST_ANCHOR_BASE64=MIICaTCCAg+gAwIBAgIBATAKBggqhkjOPQQDAjBtMQswCQYDVQQGEwJE # DEBUG_TEST_IDS_ENABLED=true # -MAPS_API_KEY=MAPS_API_KEY \ No newline at end of file +MAPS_API_KEY=MAPS_API_KEY +# +kotlin.jvm.target.validation.mode=IGNORE +# This is needed to make BuildConfig available for all modules +android.defaults.buildfeatures.buildconfig=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5f5824b2..84eed62b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ #Tue Nov 09 23:18:41 CET 2021 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +# distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/plugins/dependencies/build.gradle.kts b/plugins/dependencies/build.gradle.kts index a0bb10e3..c16cbea2 100644 --- a/plugins/dependencies/build.gradle.kts +++ b/plugins/dependencies/build.gradle.kts @@ -17,5 +17,5 @@ gradlePlugin { } dependencies { - implementation("com.android.tools.build:gradle:7.2.0") + implementation("com.android.tools.build:gradle:8.1.2") } 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 bfacbe9f..1af393b2 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 @@ -21,8 +21,12 @@ package de.gematik.ti.erp import com.android.build.gradle.AppPlugin +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.LibraryPlugin import com.android.build.gradle.internal.dsl.BaseAppModuleExtension -import org.gradle.api.JavaVersion +import de.gematik.ti.erp.Dependencies.Versions.JavaVersion +import de.gematik.ti.erp.Dependencies.Versions.SdkVersions import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.getByType @@ -35,23 +39,33 @@ class AppDependenciesPlugin : Plugin { override fun apply(project: Project) { // set android & compose options for all android plugins: `android { ... }` project.plugins.all { - if (this is AppPlugin) { - project.extensions.getByType(BaseAppModuleExtension::class).apply { - composeOptions.kotlinCompilerExtensionVersion = "1.4.6" - buildFeatures { - compose = true + when (this) { + is AppPlugin -> { + project.extensions.getByType(BaseAppModuleExtension::class).apply { + applyBaseProperties() + project.forceLibraryVersions() + androidResources { + noCompress.addAll(listOf("srt", "csv", "json")) + } + compileSdk = SdkVersions.COMPILE_SDK_VERSION + // done only for app-module and not for lib modules since it throws error + compileOptions { + isCoreLibraryDesugaringEnabled = true + } + defaultConfig { + testInstrumentationRunnerArguments += "clearPackageData" to "true" + testInstrumentationRunnerArguments += "useTestStorageService" to "true" + } } - compileSdk = Dependencies.CompileSdkVersion - defaultConfig { - minSdk = Dependencies.MinimumSdkVersion - targetSdk = Dependencies.TargetSdkVersion - } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + } + } + when (this) { + is LibraryPlugin -> { + project.extensions.getByType(LibraryExtension::class).apply { + applyBaseProperties() + project.forceLibraryVersions() + compileSdk = SdkVersions.COMPILE_SDK_VERSION } - buildToolsVersion = "33.0.1" } } } @@ -63,239 +77,47 @@ class AppDependenciesPlugin : Plugin { } } - object Dependencies { - const val MinimumSdkVersion = 24 - const val CompileSdkVersion = 33 - const val TargetSdkVersion = 33 - - object DependencyInjection { - fun kodein(module: String) = "org.kodein.di:kodein-$module:7.16.0" - } + companion object { - object Tracker + const val APP_NAME_SPACE = "de.gematik.ti.erp.app" + const val APP_ID = "de.gematik.ti.erp.app" - object DataMatrix { - const val mlkitBarcodeScanner = "com.google.mlkit:barcode-scanning:17.0.2" - - // Zxing - used for generating 2d data matrix codes - const val zxing = "com.google.zxing:core:3.5.1" - } - - object KotlinX { - fun coroutines(target: String) = "org.jetbrains.kotlinx:kotlinx-coroutines-$target:1.7.1" - const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0" - object Test { - val coroutinesTest = coroutines("test") + private fun Project.forceLibraryVersions() { + configurations.all { + resolutionStrategy { + // Forcing this lib to be of this version for all modules since later version needs CompileSdk = 34 + force("androidx.emoji2:emoji2:1.3.0") + } } } - object Lottie { - 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.1.0" - val maps = gms("maps", "18.1.0") - private fun gms(module: String, version: String) = - "com.google.android.gms:play-services-$module:$version" - - const val appReview = "com.google.android.play:review-ktx:2.0.1" - const val appUpdate = "com.google.android.play:app-update-ktx:2.0.1" - } - - object Android { - const val desugaring = "com.android.tools:desugar_jdk_libs:1.1.5" - 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.9.0" - const val datastorePreferences = "androidx.datastore:datastore-preferences:1.1.0-alpha04" - const val biometric = "androidx.biometric:biometric:1.1.0" - 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.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") - - private fun gmaps(module: String, version: String) = - "com.google.maps.android:$module:$version" - - fun lifecycle(module: String) = "androidx.lifecycle:lifecycle-$module:2.5.1" - - 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-alpha17" - - 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.3.2" - - object Test { - const val runner = "androidx.test:runner:1.5.2" - const val shotRunner = "com.karumi.shot.ShotTestRunner:6.0.0" - 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.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" + // these are common to app and library modules + private fun BaseExtension.applyBaseProperties() { + buildToolsVersion = "33.0.1" + composeOptions.kotlinCompilerExtensionVersion = "1.5.3" + defaultConfig { + minSdk = SdkVersions.MIN_SDK_VERSION + targetSdk = SdkVersions.TARGET_SDK_VERSION } - } - - object AndroidX { - 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.6" - } - - object Serialization { - const val kotlinXJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" - const val fhir = "ca.uhn.hapi.fhir:hapi-fhir-structures-r4:5.5.1" - } - - object Crypto { - const val jose4j = "org.bitbucket.b_c:jose4j:0.9.2" - - fun bouncyCastle(provider: String, targetPlatform: String = "jdk18on") = - "org.bouncycastle:$provider-$targetPlatform:1.74" - } - - object Network { - fun retrofit2(module: String) = "com.squareup.retrofit2:$module:2.9.0" - const val retrofit2KotlinXSerialization = - "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" - fun okhttp3(module: String) = "com.squareup.okhttp3:$module:4.10.0" - - // To work around a vulnerable Okio version 3.1.0 (CVE-2023-3635) we include a newer, non-vulnerable version - // to be selected by Gradle instead instead of the old one. Can be removed as soon as Retrofit releases a - // new version >2.9.0. - const val okio = "com.squareup.okio:okio:3.4.0" - object Test { - val mockWebServer = okhttp3("mockwebserver") + compileOptions { + sourceCompatibility = JavaVersion.PROJECT_JAVA_VERSION + targetCompatibility = JavaVersion.PROJECT_JAVA_VERSION } - } - - object Database { - const val realm = "io.realm.kotlin:library-base:1.7.1" - } - - const val composeVersion = "1.5.0-beta02" - - object Compose { - const val compiler = "androidx.compose.compiler:compiler:$composeVersion" - const val animation = "androidx.compose.animation:animation:$composeVersion" - const val foundation = "androidx.compose.foundation:foundation:$composeVersion" - const val material = "androidx.compose.material:material:$composeVersion" - const val runtime = "androidx.compose.runtime:runtime:$composeVersion" - const val ui = "androidx.compose.ui:ui:$composeVersion" - const val uiTooling = "androidx.compose.ui:ui-tooling:$composeVersion" - const val preview = "androidx.compose.ui:ui-tooling-preview:$composeVersion" - const val materialIcons = - "androidx.compose.material:material-icons-core:$composeVersion" - const val materialIconsExtended = - "androidx.compose.material:material-icons-extended:$composeVersion" - - fun accompanist(module: String) = "com.google.accompanist:accompanist-$module:0.28.0" - - object Test { - const val ui = "androidx.compose.ui:ui-test:$composeVersion" - const val uiManifest = "androidx.compose.ui:ui-test-manifest:$composeVersion" - const val junit4 = "androidx.compose.ui:ui-test-junit4:$composeVersion" + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } + packagingOptions { + resources { + excludes += "META-INF/**" + // for JNA and JNA-platform + excludes += "META-INF/AL2.0" + excludes += "META-INF/LGPL2.1" + // for byte-buddy + excludes += "META-INF/licenses/ASM" + pickFirsts += "win32-x86-64/attach_hotspot_windows.dll" + pickFirsts += "win32-x86/attach_hotspot_windows.dll" + } } - } - - object PasswordStrength { - const val zxcvbn = "com.nulab-inc:zxcvbn:1.7.0" - } - - object ContentSquare { - const val cts = "com.contentsquare.android:library:4.15.0" - } - - object Test { - 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" - const val shotTests = "com.karumi:shot:6.0.0" } } } - -object App { - fun dependencyInjection(init: AppDependenciesPlugin.Dependencies.DependencyInjection.() -> Unit) = - AppDependenciesPlugin.Dependencies.DependencyInjection.init() - - fun tracker(init: AppDependenciesPlugin.Dependencies.Tracker.() -> Unit) = - AppDependenciesPlugin.Dependencies.Tracker.init() - - fun dataMatrix(init: AppDependenciesPlugin.Dependencies.DataMatrix.() -> Unit) = - AppDependenciesPlugin.Dependencies.DataMatrix.init() - - fun kotlinX(init: AppDependenciesPlugin.Dependencies.KotlinX.() -> Unit) = - AppDependenciesPlugin.Dependencies.KotlinX.init() - - fun kotlinXTest(init: AppDependenciesPlugin.Dependencies.KotlinX.Test.() -> Unit) = - AppDependenciesPlugin.Dependencies.KotlinX.Test.init() - - fun playServices(init: AppDependenciesPlugin.Dependencies.PlayServices.() -> Unit) = - AppDependenciesPlugin.Dependencies.PlayServices.init() - - fun android(init: AppDependenciesPlugin.Dependencies.Android.() -> Unit) = - AppDependenciesPlugin.Dependencies.Android.init() - - fun androidX(init: AppDependenciesPlugin.Dependencies.AndroidX.() -> Unit) = - AppDependenciesPlugin.Dependencies.AndroidX.init() - - fun androidTest(init: AppDependenciesPlugin.Dependencies.Android.Test.() -> Unit) = - AppDependenciesPlugin.Dependencies.Android.Test.init() - - fun logging(init: AppDependenciesPlugin.Dependencies.Logging.() -> Unit) = - AppDependenciesPlugin.Dependencies.Logging.init() - - fun serialization(init: AppDependenciesPlugin.Dependencies.Serialization.() -> Unit) = - AppDependenciesPlugin.Dependencies.Serialization.init() - - fun crypto(init: AppDependenciesPlugin.Dependencies.Crypto.() -> Unit) = - AppDependenciesPlugin.Dependencies.Crypto.init() - - fun network(init: AppDependenciesPlugin.Dependencies.Network.() -> Unit) = - AppDependenciesPlugin.Dependencies.Network.init() - - fun networkTest(init: AppDependenciesPlugin.Dependencies.Network.Test.() -> Unit) = - AppDependenciesPlugin.Dependencies.Network.Test.init() - - fun database(init: AppDependenciesPlugin.Dependencies.Database.() -> Unit) = - AppDependenciesPlugin.Dependencies.Database.init() - - fun compose(init: AppDependenciesPlugin.Dependencies.Compose.() -> Unit) = - AppDependenciesPlugin.Dependencies.Compose.init() - - fun composeTest(init: AppDependenciesPlugin.Dependencies.Compose.Test.() -> Unit) = - AppDependenciesPlugin.Dependencies.Compose.Test.init() - - fun passwordStrength(init: AppDependenciesPlugin.Dependencies.PasswordStrength.() -> Unit) = - AppDependenciesPlugin.Dependencies.PasswordStrength.init() - - fun contentSquare(init: AppDependenciesPlugin.Dependencies.ContentSquare.() -> Unit) = - AppDependenciesPlugin.Dependencies.ContentSquare.init() - - fun test(init: AppDependenciesPlugin.Dependencies.Test.() -> Unit) = - AppDependenciesPlugin.Dependencies.Test.init() - - fun lottie(init: AppDependenciesPlugin.Dependencies.Lottie.() -> Unit) = - AppDependenciesPlugin.Dependencies.Lottie.init() -} - -fun app(init: App.() -> Unit) = App.init() -val app = AppDependenciesPlugin.Dependencies diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt new file mode 100644 index 00000000..da65d519 --- /dev/null +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Dependencies.kt @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT 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("MemberNameEqualsClassName") + +package de.gematik.ti.erp + +object Dependencies { + object Versions { + object SdkVersions { + const val MIN_SDK_VERSION = 26 + const val COMPILE_SDK_VERSION = 34 + const val TARGET_SDK_VERSION = 34 + } + + object JavaVersion { + const val KOTLIN_OPTIONS_JVM_TARGET = "17" + const val KOTLIN_OPTIONS_JVM_TARGET_INT = 17 + val PROJECT_JAVA_VERSION = org.gradle.api.JavaVersion.VERSION_17 + } + } + + object Accompanist { + private const val accompanist_version = "0.32.0" + + private fun accompanist(module: String) = "com.google.accompanist:accompanist-$module:$accompanist_version" + + val swipeRefresh = accompanist("swiperefresh") + val flowLayout = accompanist("flowlayout") + val pager = accompanist("pager") + val pageIndicator = accompanist("pager-indicators") + val systemUiController = accompanist("systemuicontroller") + } + + object Android { + private const val desugar_version = "2.0.3" + const val desugaring = "com.android.tools:desugar_jdk_libs:$desugar_version" + const val processPhoenix = "com.jakewharton:process-phoenix:2.1.2" // TODO: Not used + const val imageCropper = "com.github.CanHub:Android-Image-Cropper:4.3.2" // TODO: remove lib for cropping + } + + object AndroidX { + private const val appcompat_version = "1.6.1" + private const val legacy_version = "1.0.0" + private const val core_ktx_version = "1.11.0-beta02" // 1.12.0 needs compile version 34 + private const val data_store_version = "1.1.0-alpha04" // 1.1.0-alpha05 needs compile version 34 + private const val biometric_version = "1.2.0-alpha05" + private const val webkit_version = "1.7.0" // 1.8.0 needs compile version 34 + private const val security_crypto_version = "1.1.0-alpha06" + + private const val lifecycle_version = "2.6.2" // needs compile version 34 to go to a higher version + private const val compose_navigation_version = "2.6.0" // needs compile version 34 for 2.7.3 + private const val compose_activity_version = "1.7.2" + private const val compose_paging_version = "3.2.1" + private const val camerax_version = "1.3.0-beta01" // needs compile version 34 for 1.3.0-rc02 + private const val multiplatform_paging_version = "3.2.1" // 3.3.0-alpha02 needs compile version 34 + + const val appcompat = "androidx.appcompat:appcompat:$appcompat_version" + + const val legacySupport = "androidx.legacy:legacy-support-v4:$legacy_version" + + const val coreKtx = "androidx.core:core-ktx:$core_ktx_version" + + const val datastorePreferences = "androidx.datastore:datastore-preferences:$data_store_version" + + const val biometric = "androidx.biometric:biometric:$biometric_version" + + const val webkit = "androidx.webkit:webkit:$webkit_version" + + const val security = "androidx.security:security-crypto:$security_crypto_version" + + const val lifecycleViewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + const val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycle_version" + const val lifecycleComposeRuntime = "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" + + const val composeNavigation = "androidx.navigation:navigation-compose:$compose_navigation_version" + + const val composeActivity = "androidx.activity:activity-compose:$compose_activity_version" + + const val composePaging = "androidx.paging:paging-compose:$compose_paging_version" + const val multiplatformPaging = "androidx.paging:paging-common-ktx:$multiplatform_paging_version" + + const val camerax2 = "androidx.camera:camera-camera2:$camerax_version" + const val cameraxLifecycle = "androidx.camera:camera-lifecycle:$camerax_version" + const val cameraxView = "androidx.camera:camera-view:$camerax_version" + + object Test { + private const val test_runner_version = "1.6.0-alpha04" + private const val test_orchestrator_version = "1.5.0-alpha01" + private const val test_arch_core_version = "2.2.0" + private const val test_core_version = "1.6.0-alpha02" + private const val test_rules_version = "1.6.0-alpha01" + private const val test_espresso_version = "3.6.0-alpha01" + private const val test_junit_extension_version = "1.2.0-alpha01" + private const val test_navigation_version = "2.7.2" + + const val runner = "androidx.test:runner:$test_runner_version" + const val orchestrator = "androidx.test:orchestrator:$test_orchestrator_version" + const val services = "androidx.test.services:test-services:$test_orchestrator_version" + const val core = "androidx.test:core:$test_core_version" + const val rules = "androidx.test:rules:$test_rules_version" + const val espresso = "androidx.test.espresso:espresso-core:$test_espresso_version" + const val espressoIntents = "androidx.test.espresso:espresso-intents:$test_espresso_version" + const val junitExt = "androidx.test.ext:junit:$test_junit_extension_version" + + const val archCore = "androidx.arch.core:core-testing:$test_arch_core_version" + + const val navigation = "androidx.navigation:navigation-testing:$test_navigation_version" + } + + // Added due to AGP error -> https://github.com/android/android-test/issues/1755 + object Tracing { + const val tracing = "androidx.tracing:tracing:1.2.0-rc01" + } + } + + object Compose { + private const val compose_version = "1.5.0-beta02" + + const val compiler = "androidx.compose.compiler:compiler:$compose_version" + const val animation = "androidx.compose.animation:animation:$compose_version" + const val foundation = "androidx.compose.foundation:foundation:$compose_version" + const val material = "androidx.compose.material:material:$compose_version" + const val runtime = "androidx.compose.runtime:runtime:$compose_version" + const val ui = "androidx.compose.ui:ui:$compose_version" + const val uiTooling = "androidx.compose.ui:ui-tooling:$compose_version" + const val preview = "androidx.compose.ui:ui-tooling-preview:$compose_version" + const val materialIcons = "androidx.compose.material:material-icons-core:$compose_version" + const val materialIconsExtended = "androidx.compose.material:material-icons-extended:$compose_version" + + object Test { + const val ui = "androidx.compose.ui:ui-test:$compose_version" + const val uiManifest = "androidx.compose.ui:ui-test-manifest:$compose_version" + const val junit4 = "androidx.compose.ui:ui-test-junit4:$compose_version" + } + } + + object Coroutines { + private const val coroutines_version = "1.7.3" + private fun coroutines(target: String) = "org.jetbrains.kotlinx:kotlinx-coroutines-$target:$coroutines_version" + + val coroutinesCore = coroutines("core") + val coroutinesAndroid = coroutines("android") + val coroutinesPlayServices = coroutines("play-services") + val coroutinesSwing = coroutines("swing") + + object Test { + val coroutinesTest = coroutines("test") + } + } + + object Crypto { + private const val json_web_token_version = "0.9.3" + private const val bouncy_castle_version = "1.76" + + const val jose4j = "org.bitbucket.b_c:jose4j:$json_web_token_version" + + const val bouncycastleBcprov = "org.bouncycastle:bcprov-jdk18on:$bouncy_castle_version" + const val bouncycastleBcpkix = "org.bouncycastle:bcpkix-jdk18on:$bouncy_castle_version" + } + + object Database { + // throws error if we go to a higher version (project-issue) + private const val realm_version = "1.8.0" + + const val realm = "io.realm.kotlin:library-base:$realm_version" + } + + object DataMatrix { + private const val ml_kit_version = "17.2.0" + private const val zxing_version = "3.5.2" + + // TODO: Get rid of this lib and use a non-google safer lib like zing (its just below) + const val mlkitBarcodeScanner = "com.google.mlkit:barcode-scanning:$ml_kit_version" + + // Zxing - used for generating 2d data matrix codes + const val zxing = "com.google.zxing:core:$zxing_version" + } + + object Datetime { + private const val datetime_version = "0.4.1" + + const val datetime = "org.jetbrains.kotlinx:kotlinx-datetime:$datetime_version" + } + + object DependencyInjection { + private const val kodein_version = "7.20.2" + + val kodeinCompose = kodein("di-framework-compose") + val kodeinViewModel = kodein("di-framework-android-x-viewmodel") + val kodeinSavedState = kodein("di-framework-android-x-viewmodel-savedstate") + private fun kodein(module: String) = "org.kodein.di:kodein-$module:$kodein_version" + } + + // Should move to open-street-maps + object GoogleMaps { + private const val maps_version = "18.1.0" + private const val maps_ktx_version = "4.0.0" + private const val location_version = "21.0.1" + private const val maps_compose_version = "2.15.0" // 3.1.1 needs core_ktx_version=1.12.0 + + val maps = gms("maps", maps_version) + val location = gms("location", location_version) + + val mapsCompose = gmaps("maps-compose", maps_compose_version) + val mapsKtx = gmaps("maps-ktx", maps_ktx_version) + val mapsAndroidUtils = gmaps("maps-utils-ktx", maps_ktx_version) + private fun gms(module: String, version: String) = + "com.google.android.gms:play-services-$module:$version" + + private fun gmaps(module: String, version: String) = + "com.google.maps.android:$module:$version" + } + + object Logging { + private const val napier_version = "2.6.1" + private const val slf4j_version = "2.0.9" + + const val napier = "io.github.aakira:napier:$napier_version" + + // for desktop + const val slf4jNoOp = "org.slf4j:slf4j-nop:$slf4j_version" + } + + object Lottie { + private const val lottie_version = "6.1.0" + + const val lottie = "com.airbnb.android:lottie-compose:$lottie_version" + } + + object Network { + private const val retrofit_version = "2.9.0" + private const val retrofit_serialization_version = "1.0.0" + private const val okio_version = "3.6.0" + private const val okhttp_version = "5.0.0-alpha.11" + + const val retrofit = "com.squareup.retrofit2:retrofit:$retrofit_version" + const val retrofit2KotlinXSerialization = + "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:$retrofit_serialization_version" + + const val okhttp3 = "com.squareup.okhttp3:okhttp:$okhttp_version" + const val okhttpLogging = "com.squareup.okhttp3:logging-interceptor:$okhttp_version" + + // To work around a vulnerable Okio version 3.1.0 (CVE-2023-3635) we include a newer, non-vulnerable version + // to be selected by Gradle instead instead of the old one. Can be removed as soon as Retrofit releases a + // new version >2.9.0. + const val okio = "com.squareup.okio:okio:$okio_version" + + object Test { + const val mockWebServer = "com.squareup.okhttp3:mockwebserver:$okhttp_version" + } + } + + object PlayServices { + private const val integrity_version = "1.2.0" + private const val app_review_version = "2.0.1" + + const val integrity = "com.google.android.play:integrity:$integrity_version" + const val appReview = "com.google.android.play:review-ktx:$app_review_version" + const val appUpdate = "com.google.android.play:app-update-ktx:$app_review_version" + } + + object Serialization { + private const val kotlinx_serialization_version = "1.6.0" + private const val fhir_serialization_version = "6.8.3" + + const val kotlinXJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version" + const val kotlinXCore = "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.6.0" + + const val fhir = "ca.uhn.hapi.fhir:hapi-fhir-structures-r4:$fhir_serialization_version" + } + + object PasswordStrength { + private const val zxcvbn_version = "1.8.2" + + const val zxcvbn = "com.nulab-inc:zxcvbn:$zxcvbn_version" + } + + object Tracking { + private const val content_square_version = "4.21.0" + + const val contentSquare = "com.contentsquare.android:library:$content_square_version" + } + + object Test { + private const val mockk_old_version = "1.13.7" + private const val mockk_version = "1.13.8" + private const val junit_version = "4.13.2" + private const val snake_yaml__version = "2.2" + private const val json_version = "20231013" + private const val kotlin_test_version = "1.9.20-RC" + + const val junit4 = "junit:junit:$junit_version" + + // need a separate method to maintain a different version for common, android + const val mockkOld = "io.mockk:mockk:$mockk_old_version" + const val mockk = "io.mockk:mockk:$mockk_version" + const val mockkAndroid = "io.mockk:mockk-android:$mockk_version" + const val snakeyaml = "org.yaml:snakeyaml:$snake_yaml__version" + + const val json = "org.json:json:$json_version" + + const val kotlinTest = "org.jetbrains.kotlin:kotlin-test:$kotlin_test_version" + const val kotlinTestCommon = "org.jetbrains.kotlin:kotlin-test-common:$kotlin_test_version" + + const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:$kotlin_test_version" + } +} + +val app = Dependencies diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt new file mode 100644 index 00000000..a4eaa6f3 --- /dev/null +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/DependencyInjector.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 gematik GmbH + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the Licence); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the Licence is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and + * limitations under the Licence. + * + */ + +@file:Suppress("TooManyFunctions") + +package de.gematik.ti.erp + +object DependencyInjector { + + fun accompanist(init: Dependencies.Accompanist.() -> Unit) = + Dependencies.Accompanist.init() + + fun android(init: Dependencies.Android.() -> Unit) = + Dependencies.Android.init() + + fun androidX(init: Dependencies.AndroidX.() -> Unit) = + Dependencies.AndroidX.init() + + fun androidXTest(init: Dependencies.AndroidX.Test.() -> Unit) = + Dependencies.AndroidX.Test.init() + + fun compose(init: Dependencies.Compose.() -> Unit) = + Dependencies.Compose.init() + + fun composeTest(init: Dependencies.Compose.Test.() -> Unit) = + Dependencies.Compose.Test.init() + + fun coroutines(init: Dependencies.Coroutines.() -> Unit) = + Dependencies.Coroutines.init() + + fun coroutinesTest(init: Dependencies.Coroutines.Test.() -> Unit) = + Dependencies.Coroutines.Test.init() + + fun dependencyInjection(init: Dependencies.DependencyInjection.() -> Unit) = + Dependencies.DependencyInjection.init() + + fun dataMatrix(init: Dependencies.DataMatrix.() -> Unit) = + Dependencies.DataMatrix.init() + + fun dateTime(init: Dependencies.Datetime.() -> Unit) = + Dependencies.Datetime.init() + + fun playServices(init: Dependencies.PlayServices.() -> Unit) = + Dependencies.PlayServices.init() + + fun logging(init: Dependencies.Logging.() -> Unit) = + Dependencies.Logging.init() + + fun serialization(init: Dependencies.Serialization.() -> Unit) = + Dependencies.Serialization.init() + + fun crypto(init: Dependencies.Crypto.() -> Unit) = + Dependencies.Crypto.init() + + fun network(init: Dependencies.Network.() -> Unit) = + Dependencies.Network.init() + + fun networkTest(init: Dependencies.Network.Test.() -> Unit) = + Dependencies.Network.Test.init() + + fun database(init: Dependencies.Database.() -> Unit) = + Dependencies.Database.init() + + fun passwordStrength(init: Dependencies.PasswordStrength.() -> Unit) = + Dependencies.PasswordStrength.init() + + fun tracking(init: Dependencies.Tracking.() -> Unit) = + Dependencies.Tracking.init() + + fun test(init: Dependencies.Test.() -> Unit) = + Dependencies.Test.init() + + fun lottie(init: Dependencies.Lottie.() -> Unit) = + Dependencies.Lottie.init() + + // Should move to open-street-maps + fun maps(init: Dependencies.GoogleMaps.() -> Unit) = + Dependencies.GoogleMaps.init() + + fun tracing(init: Dependencies.AndroidX.Tracing.() -> Unit) = + Dependencies.AndroidX.Tracing.init() +} + +fun inject(init: DependencyInjector.() -> Unit) = DependencyInjector.init() diff --git a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt similarity index 86% rename from plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt rename to plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt index 87a8ef28..ae92b5ce 100644 --- a/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/Overriding.kt +++ b/plugins/dependencies/src/main/kotlin/de/gematik/ti/erp/ProjectOverriding.kt @@ -23,10 +23,13 @@ import org.gradle.kotlin.dsl.getPlugin import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty +// For version code and version name fun Project.overriding() = PropertyDelegateProvider { _: Any?, _ -> - ReadOnlyProperty { ref, property -> - project.plugins.getPlugin(AppDependenciesPlugin::class).overrideProperties.getProperty(property.name) + ReadOnlyProperty { _, property -> + project.plugins.getPlugin(AppDependenciesPlugin::class) + .overrideProperties + .getProperty(property.name) ?: (project.properties[property.name] as? String) ?: "" } diff --git a/plugins/resource-generation/build.gradle.kts b/plugins/resource-generation/build.gradle.kts index 03f93fe8..32bdbc87 100644 --- a/plugins/resource-generation/build.gradle.kts +++ b/plugins/resource-generation/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` - kotlin("jvm") version "1.8.0" + kotlin("jvm") version "1.7.0" kotlin("plugin.serialization") version "1.8.0" `java-gradle-plugin` } diff --git a/plugins/technical-requirements-plugin/build.gradle.kts b/plugins/technical-requirements-plugin/build.gradle.kts index dc5b1d61..51850255 100644 --- a/plugins/technical-requirements-plugin/build.gradle.kts +++ b/plugins/technical-requirements-plugin/build.gradle.kts @@ -10,7 +10,7 @@ tasks.withType() { } dependencies { - implementation("com.android.tools.build:gradle:7.2.0") + implementation("com.android.tools.build:gradle:8.1.2") } gradlePlugin { diff --git a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt index 2bf631b0..41bffbfd 100644 --- a/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt +++ b/plugins/technical-requirements-plugin/src/main/kotlin/de/gematik/ti/erp/gradleplugins/TechnicalRequirementsPlugin.kt @@ -24,16 +24,16 @@ import org.gradle.api.Project import java.io.File class TechnicalRequirementsPlugin : Plugin { - var project: Project? = null + private var project: Project? = null override fun apply(project: Project) { this.project = project project.tasks.register("generateTechnicalRequirementsMarkdown") { doLast { val sourceDirs = setOf( - File(project.rootDir.path + "/android/src/main/java/de/gematik/ti/erp/app"), + File(project.rootDir.path + "/app/android/src/main/java/de/gematik/ti/erp/app"), + File(project.rootDir.path + "/app/feature/src/main/kotlin/de/gematik/ti/erp/app"), File(project.rootDir.path + "/common/src/commonMain/kotlin/de/gematik/ti/erp/app") ) - // println(sourceDirs) processSourceDirectory(sourceDirs) } } @@ -47,13 +47,9 @@ class TechnicalRequirementsPlugin : Plugin { files.forEach { file -> val annotationData = findAnnotationsInFile(file) - annotationData.forEach { - // println(it) - } allAnnotations.addAll(annotationData) } } - generateHtmlReport(allAnnotations) } @@ -68,7 +64,6 @@ class TechnicalRequirementsPlugin : Plugin { val matches = ANNOTATION_REGEX.findAll(content) for (match in matches) { val annotationText = match.value - // println(annotationText) val startLine = content.substring(0, match.range.first).count { it == '\n' } + 1 val requirements = parseRequirements(annotationText) @@ -128,7 +123,6 @@ class TechnicalRequirementsPlugin : Plugin { val match = RATIONALE_REGEX.find(annotationText) println(annotationText) println(match?.value ?: "_________") - return match?.value ?: "" } @@ -166,7 +160,7 @@ class TechnicalRequirementsPlugin : Plugin { htmlBuilder.append("

$specification

") htmlBuilder.append("
    ") val annotationDataList = annotationList.filter { it.specification == specification } - .sortedWith(compareBy { it.requirement }.thenBy { it.extractSuffixNumber() ?: 0 }) + .sortedWith(compareBy { it.requirement }.thenBy { it.extractSuffixNumber() }) for (data in annotationDataList) { htmlBuilder.append("
  • ") htmlBuilder.append( diff --git a/rules/build.gradle.kts b/rules/build.gradle.kts index 9bef9421..869f2ee4 100644 --- a/rules/build.gradle.kts +++ b/rules/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.8.0" + kotlin("jvm") version "1.7.0" } repositories { diff --git a/settings.gradle.kts b/settings.gradle.kts index 5f522dda..15008c2e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,9 +8,6 @@ pluginManagement { } resolutionStrategy { eachPlugin { - if (requested.id.id == "dagger.hilt.android") { - useModule("com.google.dagger:hilt-android-gradle-plugin:${requested.version}") - } if (requested.id.id == "com.codingfeline.buildkonfig") { useModule("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:${requested.version}") } @@ -51,6 +48,13 @@ includeBuild("smartcard-wrapper") { // } //} -include("android", "desktop", "common", "plugins:technical-requirements-plugin") +include(":app:android") +include(":app:android-mock") +include(":app:features") +include(":app:shared-test") +include(":app:demo-mode") +include(":common") +include(":desktop") +include(":plugins:technical-requirements-plugin") rootProject.name = "E-Rezept"