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.
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