From 5e86439f49f67e5c3b799869f070946bd878b594 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Mon, 21 Oct 2024 13:04:56 -0700 Subject: [PATCH 1/2] SpeziOnboarding --- .../onboarding/OnboardingNavigationEvent.kt | 1 - .../onboarding/consent/ConsentDocument.kt | 21 ++++++++++ .../consent/ConsentDocumentExport.kt | 10 +++++ .../onboarding/consent/ConsentViewState.kt | 13 ++++++ .../onboarding/consent/ExportConfiguration.kt | 39 ++++++++++++++++++ ...reen.kt => OnboardingConsentComposable.kt} | 9 +++- .../module/onboarding/consent/ViewState.kt | 7 ++++ .../onboarding/OnboardingComposableBuilder.kt | 41 +++++++++++++++++++ .../onboarding/onboarding/OnboardingStack.kt | 4 ++ 9 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/{ConsentScreen.kt => OnboardingConsentComposable.kt} (89%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt index 084bb6644..cef78a7f1 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/OnboardingNavigationEvent.kt @@ -3,7 +3,6 @@ package edu.stanford.spezi.module.onboarding import edu.stanford.spezi.core.navigation.NavigationEvent sealed class OnboardingNavigationEvent : NavigationEvent { - data object InvitationCodeScreen : OnboardingNavigationEvent() data class OnboardingScreen(val clearBackStack: Boolean) : OnboardingNavigationEvent() data object SequentialOnboardingScreen : OnboardingNavigationEvent() diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt new file mode 100644 index 000000000..c907d3973 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -0,0 +1,21 @@ +package edu.stanford.spezi.module.onboarding.consent + +import androidx.compose.runtime.MutableState +import edu.stanford.spezi.core.design.component.StringResource + +data class ConsentDocument( + val markdown: suspend () -> ByteArray, + val viewState: MutableState, + val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, + val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, + val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, + val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, + val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), +) { + object LocalizationDefaults { + val givenNameTitle = StringResource("Given Name") + val givenNamePlaceholder = StringResource("Given Name Placeholder") + val familyNameTitle = StringResource("Family Name") + val familyNamePlaceholder = StringResource("Family Name Placeholder") + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt new file mode 100644 index 000000000..1d60d6c43 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt @@ -0,0 +1,10 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument + +class ConsentDocumentExport( + private val documentIdentifier: String, + private val document: suspend () -> PdfDocument +) { + suspend fun createDocument() = document() +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt new file mode 100644 index 000000000..79998c259 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt @@ -0,0 +1,13 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument + +sealed interface ConsentViewState { + data class Base(val viewState: ViewState) : ConsentViewState + data object NamesEntered : ConsentViewState + data object Signing : ConsentViewState + data object Signed : ConsentViewState + data object Export : ConsentViewState + data class Exported(val document: PdfDocument) : ConsentViewState + data object Storing : ConsentViewState +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt new file mode 100644 index 000000000..a15eef4b6 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt @@ -0,0 +1,39 @@ +package edu.stanford.spezi.module.onboarding.consent + +import edu.stanford.spezi.core.design.component.StringResource + +data class ConsentDocumentExportConfiguration( + val paperSize: PaperSize = PaperSize.usLetter, + val consentTitle: StringResource = LocalizationDefaults.exportedConsentFormTitle, + val includingTimestamp: Boolean = true, +) { + object LocalizationDefaults { + val exportedConsentFormTitle = StringResource("Consent") + } + + data class PaperSize( + val width: Double, + val height: Double, + ) { + companion object { + private const val A4_WIDTH_IN_INCHES = 8.3 + private const val A4_HEIGHT_IN_INCHES = 11.7 + + private const val US_LETTER_WIDTH_IN_INCHES = 8.5 + private const val US_LETTER_HEIGHT_IN_INCHES = 11.0 + + val usLetter get() = usLetter() + val dinA4 get() = dinA4() + + fun dinA4(pointsPerInch: Double = 72.0) = PaperSize( + width = A4_WIDTH_IN_INCHES * pointsPerInch, + height = A4_HEIGHT_IN_INCHES * pointsPerInch + ) + + fun usLetter(pointsPerInch: Double = 72.0) = PaperSize( + width = US_LETTER_WIDTH_IN_INCHES * pointsPerInch, + height = US_LETTER_HEIGHT_IN_INCHES * pointsPerInch + ) + } + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt similarity index 89% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index f72326e1e..e13974727 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentScreen.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings @@ -22,7 +23,13 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.utils.extensions.testIdentifier @Composable -fun ConsentScreen() { +fun OnboardingConsentComposable( + markdown: suspend () -> ByteArray, + action: suspend () -> Unit, + title: StringResource? = StringResource("Consent"), + identifier: String = "ConsentDocument", + exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), +) { val viewModel: ConsentViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt new file mode 100644 index 000000000..2253fc5ca --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt @@ -0,0 +1,7 @@ +package edu.stanford.spezi.module.onboarding.consent + +sealed interface ViewState { + data object Idle : ViewState + data object Processing : ViewState + data class Error(val throwable: Throwable?) : ViewState +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt new file mode 100644 index 000000000..c32b6e9de --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposableBuilder.kt @@ -0,0 +1,41 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +data class OnboardingStep( + val identifier: String, + val composable: @Composable () -> Unit +) + +data class OnboardingComposableBuilder( + var list: MutableList +) { + fun step(id: String, composable: @Composable () -> Unit) { + list.add(OnboardingStep(id, composable)) + } +} + +fun buildOnboardingSteps( + build: OnboardingComposableBuilder.() -> Unit +): List { + val builder = OnboardingComposableBuilder(mutableListOf()) + build(builder) + return builder.list +} + +fun test() { + buildOnboardingSteps { + step("") { + Text("") + } + + val bool = true + + if (bool) { + step("check") { + Text("check") + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt new file mode 100644 index 000000000..4d1d6a98a --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt @@ -0,0 +1,4 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +class OnboardingStack { +} \ No newline at end of file From bfc3a6faf9058c2b3d75b6c8d7a359ae32214f14 Mon Sep 17 00:00:00 2001 From: Paul Kraft Date: Thu, 24 Oct 2024 10:06:13 -0700 Subject: [PATCH 2/2] update --- .../onboarding/EngageConsentManager.kt | 1 - .../engagehf/onboarding/OnboardingModule.kt | 1 - .../component/markdown/MarkdownComposable.kt | 10 ++ .../component/markdown/MarkdownParser.kt | 1 - .../component/markdown/MarkdownUiState.kt | 5 + .../component/markdown/MarkdownViewModel.kt | 29 ++++ .../utils/foundation}/PersonNameComponents.kt | 2 +- .../spezi/modules/contact/ContactFactory.kt | 2 +- .../simulator/ContactComposableSimulator.kt | 2 +- .../modules/contact/ContactComposable.kt | 2 +- .../spezi/modules/contact/model/Contact.kt | 1 + .../onboarding/di/TestOnboardingModule.kt | 1 - .../onboarding/consent/ConsentConstraint.kt | 7 + .../onboarding/consent/ConsentDataSource.kt | 22 +++ .../onboarding/consent/ConsentDocument.kt | 153 +++++++++++++++++- .../consent/ConsentDocumentExport.kt | 8 +- .../onboarding/consent/ConsentManager.kt | 11 -- ...reationService.kt => ConsentPdfService.kt} | 36 +++-- .../onboarding/consent/ConsentUiState.kt | 15 +- .../onboarding/consent/ConsentViewModel.kt | 47 +++--- .../onboarding/consent/ConsentViewState.kt | 1 + .../onboarding/consent/ExportConfiguration.kt | 6 +- .../consent/OnboardingConsentComposable.kt | 99 +++++++++--- .../module/onboarding/consent/SignaturePad.kt | 138 ---------------- .../module/onboarding/consent/ViewState.kt | 7 - .../onboarding/OnboardingActions.kt | 48 ++++++ .../onboarding/OnboardingComposable.kt | 43 +++++ .../onboarding/onboarding/OnboardingStack.kt | 3 - .../onboarding/onboarding/OnboardingTitle.kt | 37 +++++ .../flow/IllegalOnboardingStepComposable.kt | 10 ++ .../spezi/module/onboarding/views/Standard.kt | 3 + .../module/onboarding/views/SuspendButton.kt | 61 +++++++ .../module/onboarding/views/ViewState.kt | 17 ++ .../module/onboarding/views/ViewStateAlert.kt | 33 ++++ 34 files changed, 613 insertions(+), 249 deletions(-) create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt create mode 100644 core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt rename {modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model => core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation}/PersonNameComponents.kt (91%) create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt rename modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/{PdfCreationService.kt => ConsentPdfService.kt} (79%) delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt delete mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt create mode 100644 modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt index 784f71d44..9e2233a07 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/EngageConsentManager.kt @@ -3,7 +3,6 @@ package edu.stanford.bdh.engagehf.onboarding import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent import edu.stanford.spezi.core.navigation.Navigator import edu.stanford.spezi.core.utils.MessageNotifier -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import javax.inject.Inject class EngageConsentManager @Inject internal constructor( diff --git a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt index a59c013e6..73edc50ff 100644 --- a/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt +++ b/app/src/main/kotlin/edu/stanford/bdh/engagehf/onboarding/OnboardingModule.kt @@ -4,7 +4,6 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingRepository diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt index a01503e04..be2d1ae6d 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownComposable.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -14,6 +15,15 @@ import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles +@Composable +fun MarkdownComposable(data: suspend () -> ByteArray) { + // TODO: Figure out why hiltViewModel is not working and how one would do that anyways + val viewModel = remember { MarkdownViewModel(data, MarkdownParser()) } + val uiState = viewModel.uiState.collectAsState() + + MarkdownComponent(uiState.value.elements ?: emptyList()) +} + @Composable fun MarkdownComponent(markdownElements: List) { LazyColumn(modifier = Modifier.padding(Spacings.medium)) { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt index f5673f699..e28cbf74a 100644 --- a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownParser.kt @@ -7,7 +7,6 @@ private const val HEADING_LEVEL_2 = 2 private const val HEADING_LEVEL_3 = 3 class MarkdownParser @Inject constructor() { - fun parse(text: String): List = buildList { text.lines().forEach { line -> when { diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt new file mode 100644 index 000000000..87ed6e178 --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownUiState.kt @@ -0,0 +1,5 @@ +package edu.stanford.spezi.core.design.component.markdown + +data class MarkdownUiState( + val elements: List? = null +) \ No newline at end of file diff --git a/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt new file mode 100644 index 000000000..97f2cc80e --- /dev/null +++ b/core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/markdown/MarkdownViewModel.kt @@ -0,0 +1,29 @@ +package edu.stanford.spezi.core.design.component.markdown + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.nio.charset.StandardCharsets + +internal class MarkdownViewModel @AssistedInject internal constructor( + @Assisted private val data: suspend () -> ByteArray, + private val markdownParser: MarkdownParser, +) : ViewModel() { + private val _uiState = MutableStateFlow(MarkdownUiState()) + val uiState: StateFlow = _uiState + + init { + viewModelScope.launch { + val markdownText = data().toString(StandardCharsets.UTF_8) + val markdownElements = markdownParser.parse(markdownText) + _uiState.update { + it.copy(elements = markdownElements) + } + } + } +} diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt similarity index 91% rename from modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt rename to core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt index d2987a340..6cc517be3 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/PersonNameComponents.kt +++ b/core/utils/src/main/kotlin/edu/stanford/spezi/core/utils/foundation/PersonNameComponents.kt @@ -1,4 +1,4 @@ -package edu.stanford.spezi.modules.contact.model +package edu.stanford.spezi.core.utils.foundation data class PersonNameComponents( val namePrefix: String? = null, diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt index a0c2259ab..95e2accee 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/ContactFactory.kt @@ -4,9 +4,9 @@ import android.location.Address import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBox import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text diff --git a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt index 3aee4bdf1..bb68474c1 100644 --- a/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt +++ b/modules/contact/src/androidTest/kotlin/edu/stanford/spezi/modules/contact/simulator/ContactComposableSimulator.kt @@ -10,9 +10,9 @@ import androidx.compose.ui.test.onChildAt import androidx.test.platform.app.InstrumentationRegistry import edu.stanford.spezi.core.design.component.StringResource import edu.stanford.spezi.core.testing.onNodeWithIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.ContactComposableTestIdentifier import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.formatted class ContactComposableSimulator( diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt index 2c2fbeff4..a6bcd1cb5 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/ContactComposable.kt @@ -33,11 +33,11 @@ import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.design.theme.TextStyles import edu.stanford.spezi.core.design.theme.ThemePreviews import edu.stanford.spezi.core.utils.extensions.testIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import edu.stanford.spezi.modules.contact.component.AddressCard import edu.stanford.spezi.modules.contact.component.ContactOptionCard import edu.stanford.spezi.modules.contact.model.Contact import edu.stanford.spezi.modules.contact.model.ContactOption -import edu.stanford.spezi.modules.contact.model.PersonNameComponents import edu.stanford.spezi.modules.contact.model.call import edu.stanford.spezi.modules.contact.model.email import edu.stanford.spezi.modules.contact.model.text diff --git a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt index cef15dfeb..181d8faa9 100644 --- a/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt +++ b/modules/contact/src/main/kotlin/edu/stanford/spezi/modules/contact/model/Contact.kt @@ -3,6 +3,7 @@ package edu.stanford.spezi.modules.contact.model import android.location.Address import androidx.compose.ui.graphics.vector.ImageVector import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import java.util.UUID /** diff --git a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt index 96d631f97..1c7bf87de 100644 --- a/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt +++ b/modules/onboarding/src/androidTest/kotlin/edu/stanford/spezi/module/onboarding/di/TestOnboardingModule.kt @@ -7,7 +7,6 @@ import dagger.hilt.testing.TestInstallIn import edu.stanford.spezi.module.account.di.AccountModule import edu.stanford.spezi.module.account.manager.InvitationAuthManager import edu.stanford.spezi.module.account.manager.UserSessionManager -import edu.stanford.spezi.module.onboarding.consent.ConsentManager import edu.stanford.spezi.module.onboarding.fakes.FakeOnboardingRepository import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt new file mode 100644 index 000000000..c388f1347 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentConstraint.kt @@ -0,0 +1,7 @@ +package edu.stanford.spezi.module.onboarding.consent + +import edu.stanford.spezi.module.onboarding.views.Standard + +interface ConsentConstraint : Standard { + suspend fun store(consent: ConsentDocumentExport) +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt new file mode 100644 index 000000000..f1837b081 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDataSource.kt @@ -0,0 +1,22 @@ +package edu.stanford.spezi.module.onboarding.consent + +import android.graphics.pdf.PdfDocument +import edu.stanford.spezi.module.onboarding.views.Standard +import javax.inject.Inject + +class ConsentDataSource { + @Inject lateinit var standard: Standard + + init { + if (standard !is ConsentConstraint) { + TODO("on iOS: fatalError") + } + } + + suspend fun store(document: suspend () -> PdfDocument, identifier: String) { + (standard as? ConsentConstraint)?.let { consentConstraint -> + val export = ConsentDocumentExport(identifier, document) + consentConstraint.store(export) + } ?: TODO("on iOS: fatalError") + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt index c907d3973..71d2e45a6 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocument.kt @@ -1,16 +1,43 @@ package edu.stanford.spezi.module.onboarding.consent +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.core.design.theme.Spacings +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.views.ViewState +import java.nio.charset.StandardCharsets data class ConsentDocument( - val markdown: suspend () -> ByteArray, - val viewState: MutableState, - val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, - val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, - val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, - val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, - val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), + private val markdown: suspend () -> ByteArray, + private val viewState: MutableState, + private val givenNameTitle: StringResource = LocalizationDefaults.givenNameTitle, + private val givenNamePlaceholder: StringResource = LocalizationDefaults.givenNamePlaceholder, + private val familyNameTitle: StringResource = LocalizationDefaults.familyNameTitle, + private val familyNamePlaceholder: StringResource = LocalizationDefaults.familyNamePlaceholder, + private val exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), ) { object LocalizationDefaults { val givenNameTitle = StringResource("Given Name") @@ -18,4 +45,114 @@ data class ConsentDocument( val familyNameTitle = StringResource("Family Name") val familyNamePlaceholder = StringResource("Family Name Placeholder") } -} \ No newline at end of file + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + internal fun Composable( + modifier: Modifier = Modifier, + uiState: ConsentUiState, + onAction: (ConsentAction) -> Unit + ) { + val givenName = uiState.name.givenName ?: "" + val familyName = uiState.name.familyName ?: "" + + val keyboardController = LocalSoftwareKeyboardController.current + Column(modifier = modifier) { + OutlinedTextField( + value = givenName, + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(givenNameTitle.text()) }, + singleLine = true, + placeholder = { Text(givenNamePlaceholder.text()) }, + trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Information Icon") } + ) + Spacer(modifier = Modifier.height(Spacings.small)) + OutlinedTextField( + value = familyName, + onValueChange = { + onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.LAST_NAME)) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(familyNameTitle.text()) }, + placeholder = { Text(familyNamePlaceholder.text()) }, + singleLine = true, + trailingIcon = { + Icon( + Icons.Filled.Info, + contentDescription = "Information Icon" + ) + } + ) + + + if (givenName.isNotBlank() && familyName.isNotBlank()) { + Spacer(modifier = Modifier.height(Spacings.medium)) + Text("Signature:") + SignatureCanvas( + paths = uiState.paths.toMutableList(), + firstName = givenName, + lastName = familyName, + onPathAdd = { path -> + onAction(ConsentAction.AddPath(path)) + keyboardController?.hide() + } + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Row(modifier = Modifier.fillMaxWidth()) { + FilledTonalButton( + onClick = { + if (uiState.paths.isNotEmpty()) { + onAction(ConsentAction.Undo) + } + }, + enabled = uiState.paths.isNotEmpty(), + modifier = Modifier.weight(1f), + ) { + Text("Undo") + } + Spacer(modifier = Modifier.width(Spacings.medium)) + } + } + } + } +} + +@SuppressLint("UnrememberedMutableState") +@Preview +@Composable +private fun ConsentDocumentComposablePreview( + @PreviewParameter(ConsentDocumentComposablePreviewProvider::class) data: ConsentDocumentComposablePreviewData, +) { + ConsentDocument( + markdown = { "".toByteArray(StandardCharsets.UTF_8) }, + viewState = remember { mutableStateOf(ConsentViewState.Base(ViewState.Idle)) }, + ).Composable( + uiState = ConsentUiState( + name = data.name, + paths = data.paths + ) + ) {} +} + +private data class ConsentDocumentComposablePreviewData( + val paths: MutableList, + val name: PersonNameComponents, +) + +private class ConsentDocumentComposablePreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ConsentDocumentComposablePreviewData( + paths = mutableListOf(Path()), + name = PersonNameComponents(givenName = "", familyName = "") + ), + @Suppress("MagicNumber") + ConsentDocumentComposablePreviewData( + paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), + name = PersonNameComponents(givenName = "Jane", familyName = "Doe") + + ) + ) +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt index 1d60d6c43..022d75d50 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentDocumentExport.kt @@ -3,8 +3,12 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument class ConsentDocumentExport( - private val documentIdentifier: String, + private val documentIdentifier: String = Defaults.DOCUMENT_IDENTIFIER, private val document: suspend () -> PdfDocument ) { + private object Defaults { + const val DOCUMENT_IDENTIFIER = "ConsentDocument" + } + suspend fun createDocument() = document() -} \ No newline at end of file +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt deleted file mode 100644 index 784a4ec24..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentManager.kt +++ /dev/null @@ -1,11 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -/** - * A interface that needs to be implemented and provided by the app to provide the consent text and handle consent actions. - * @see edu.stanford.bdh.engagehf.onboarding.EngageConsentManager - */ -interface ConsentManager { - suspend fun getMarkdownText(): String - suspend fun onConsented() - suspend fun onConsentFailure(error: Throwable) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt similarity index 79% rename from modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt rename to modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt index 4b6312de1..27cc7489e 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/PdfCreationService.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentPdfService.kt @@ -8,29 +8,39 @@ import android.graphics.pdf.PdfDocument import android.text.Layout import android.text.StaticLayout import android.text.TextPaint +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asAndroidPath import edu.stanford.spezi.core.coroutines.di.Dispatching import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream import java.time.LocalDate import javax.inject.Inject -internal class PdfCreationService @Inject internal constructor( +internal class ConsentPdfService @Inject internal constructor( @Dispatching.IO private val ioCoroutineDispatcher: CoroutineDispatcher, ) { - suspend fun createPdf(uiState: ConsentUiState): ByteArray = withContext(ioCoroutineDispatcher) { + suspend fun createDocument( + configuration: ConsentDocumentExportConfiguration, + name: PersonNameComponents, + signaturePaths: List, + markdownElements: List, + ): PdfDocument = withContext(ioCoroutineDispatcher) { val pdfDocument = PdfDocument() - val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create() + val pageInfo = PdfDocument.PageInfo.Builder( + configuration.paperSize.width.toInt(), + configuration.paperSize.height.toInt(), + 1 + ).create() val page = pdfDocument.startPage(pageInfo) val canvas = page.canvas var yOffset = 50f - uiState.markdownElements.forEach { + markdownElements.forEach { yOffset = when (it) { is MarkdownElement.Heading -> drawHeading(canvas, it, yOffset) is MarkdownElement.Paragraph -> drawParagraph(canvas, it, yOffset) @@ -39,19 +49,17 @@ internal class PdfCreationService @Inject internal constructor( } } yOffset += 50f - yOffset = drawNamesAndSignature(canvas, uiState, yOffset) + yOffset = drawNameAndSignature(canvas, name, signaturePaths, yOffset) pdfDocument.finishPage(page) - val outputStream = ByteArrayOutputStream() - pdfDocument.writeTo(outputStream) - pdfDocument.close() - outputStream.toByteArray() + pdfDocument } - private fun drawNamesAndSignature( + private fun drawNameAndSignature( canvas: Canvas, - uiState: ConsentUiState, + name: PersonNameComponents, + signaturePaths: List, yOffset: Float, ): Float { val paintNames = Paint().apply { @@ -59,7 +67,7 @@ internal class PdfCreationService @Inject internal constructor( textSize = 14f } canvas.drawText( - "First Name: ${uiState.firstName.value} Last Name: ${uiState.lastName.value} Date: ${LocalDate.now()}", + "First Name: ${name.givenName ?: ""} Last Name: ${name.familyName ?: ""} Date: ${LocalDate.now()}", 10f, yOffset, paintNames @@ -74,7 +82,7 @@ internal class PdfCreationService @Inject internal constructor( canvas.save() canvas.scale(scaleFactor, scaleFactor) - uiState.paths.forEach { path -> + signaturePaths.forEach { path -> val androidPath = path.asAndroidPath() val offsetPath = android.graphics.Path(androidPath) offsetPath.offset(0f, yOffset * 5) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt index 25b60f35c..cf7a263bf 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentUiState.kt @@ -2,15 +2,17 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.compose.ui.graphics.Path import edu.stanford.spezi.core.design.component.markdown.MarkdownElement +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.views.ViewState -data class ConsentUiState( - val firstName: FieldState = FieldState(value = "", error = false), - val lastName: FieldState = FieldState(value = "", error = false), +internal data class ConsentUiState( + val name: PersonNameComponents = PersonNameComponents(), val paths: List = emptyList(), val markdownElements: List = emptyList(), + val viewState: ConsentViewState = ConsentViewState.Base(ViewState.Idle), ) { val isValidForm: Boolean = - firstName.value.isNotBlank() && lastName.value.isNotBlank() && paths.isNotEmpty() + (name.givenName?.isNotBlank() ?: false) && (name.familyName?.isNotBlank() ?: false) && paths.isNotEmpty() } data class FieldState( @@ -26,5 +28,8 @@ sealed interface ConsentAction { data class TextFieldUpdate(val newValue: String, val type: TextFieldType) : ConsentAction data class AddPath(val path: Path) : ConsentAction data object Undo : ConsentAction - data object Consent : ConsentAction + data class Consent( + val documentIdentifier: String, + val exportConfiguration: ConsentDocumentExportConfiguration + ) : ConsentAction } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt index 2fbefdab2..ffd615606 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewModel.kt @@ -3,8 +3,7 @@ package edu.stanford.spezi.module.onboarding.consent import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import edu.stanford.spezi.core.design.component.markdown.MarkdownParser -import edu.stanford.spezi.module.account.manager.UserSessionManager +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -13,35 +12,22 @@ import javax.inject.Inject @HiltViewModel internal class ConsentViewModel @Inject internal constructor( - private val consentManager: ConsentManager, - private val markdownParser: MarkdownParser, - private val pdfCreationService: PdfCreationService, - private val userSessionManager: UserSessionManager, + private val pdfService: ConsentPdfService, + private val consentDataSource: ConsentDataSource, ) : ViewModel() { private val _uiState = MutableStateFlow(ConsentUiState()) val uiState: StateFlow = _uiState - init { - viewModelScope.launch { - val markdownText = consentManager.getMarkdownText() - _uiState.update { - it.copy(markdownElements = markdownParser.parse(markdownText)) - } - } - } - fun onAction(action: ConsentAction) { when (action) { is ConsentAction.TextFieldUpdate -> { when (action.type) { TextFieldType.FIRST_NAME -> { - val firstName = FieldState(value = action.newValue) - _uiState.update { it.copy(firstName = firstName) } + _uiState.update { it.copy(name = it.name.copy(givenName = action.newValue)) } } TextFieldType.LAST_NAME -> { - val lastName = FieldState(value = action.newValue) - _uiState.update { it.copy(lastName = lastName) } + _uiState.update { it.copy(name = it.name.copy(familyName = action.newValue)) } } } } @@ -55,17 +41,20 @@ internal class ConsentViewModel @Inject internal constructor( } is ConsentAction.Consent -> { - onConsentAction() + viewModelScope.launch { + consentDataSource.store( + { + pdfService.createDocument( + action.exportConfiguration, + uiState.value.name, + uiState.value.paths, + uiState.value.markdownElements, + ) + }, + action.documentIdentifier, + ) + } } } } - - private fun onConsentAction() { - viewModelScope.launch { - val pdfBytes = pdfCreationService.createPdf(uiState = uiState.value) - userSessionManager.uploadConsentPdf(pdfBytes = pdfBytes) - .onSuccess { consentManager.onConsented() } - .onFailure { consentManager.onConsentFailure(error = it) } - } - } } diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt index 79998c259..eda047335 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ConsentViewState.kt @@ -1,6 +1,7 @@ package edu.stanford.spezi.module.onboarding.consent import android.graphics.pdf.PdfDocument +import edu.stanford.spezi.module.onboarding.views.ViewState sealed interface ConsentViewState { data class Base(val viewState: ViewState) : ConsentViewState diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt index a15eef4b6..8e21ac155 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ExportConfiguration.kt @@ -22,15 +22,17 @@ data class ConsentDocumentExportConfiguration( private const val US_LETTER_WIDTH_IN_INCHES = 8.5 private const val US_LETTER_HEIGHT_IN_INCHES = 11.0 + private const val DEFAULT_POINTS_PER_INCH = 72.0 + val usLetter get() = usLetter() val dinA4 get() = dinA4() - fun dinA4(pointsPerInch: Double = 72.0) = PaperSize( + fun dinA4(pointsPerInch: Double = DEFAULT_POINTS_PER_INCH) = PaperSize( width = A4_WIDTH_IN_INCHES * pointsPerInch, height = A4_HEIGHT_IN_INCHES * pointsPerInch ) - fun usLetter(pointsPerInch: Double = 72.0) = PaperSize( + fun usLetter(pointsPerInch: Double = DEFAULT_POINTS_PER_INCH) = PaperSize( width = US_LETTER_WIDTH_IN_INCHES * pointsPerInch, height = US_LETTER_HEIGHT_IN_INCHES * pointsPerInch ) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt index e13974727..781aa2983 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/OnboardingConsentComposable.kt @@ -4,62 +4,111 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Path import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezi.core.design.component.Button import edu.stanford.spezi.core.design.component.StringResource -import edu.stanford.spezi.core.design.component.markdown.MarkdownComponent +import edu.stanford.spezi.core.design.component.markdown.MarkdownComposable import edu.stanford.spezi.core.design.component.markdown.MarkdownElement import edu.stanford.spezi.core.design.theme.Spacings import edu.stanford.spezi.core.design.theme.SpeziTheme import edu.stanford.spezi.core.utils.extensions.testIdentifier +import edu.stanford.spezi.core.utils.foundation.PersonNameComponents +import edu.stanford.spezi.module.onboarding.onboarding.OnboardingComposable +import edu.stanford.spezi.module.onboarding.onboarding.OnboardingTitle +import edu.stanford.spezi.module.onboarding.views.ViewState +import kotlinx.coroutines.launch @Composable fun OnboardingConsentComposable( markdown: suspend () -> ByteArray, action: suspend () -> Unit, - title: StringResource? = StringResource("Consent"), - identifier: String = "ConsentDocument", - exportConfiguration: ConsentDocumentExportConfiguration = ConsentDocumentExportConfiguration(), + title: StringResource? = remember { StringResource("Consent") }, + identifier: String = remember { "ConsentDocument" }, + exportConfiguration: ConsentDocumentExportConfiguration = remember { ConsentDocumentExportConfiguration() }, ) { val viewModel: ConsentViewModel = hiltViewModel() val uiState by viewModel.uiState.collectAsState() - ConsentScreen( - onAction = viewModel::onAction, uiState = uiState + OnboardingConsentComposableContent( + markdown = markdown, + action = action, + title = title, + identifier = identifier, + exportConfiguration = exportConfiguration, + uiState = uiState, + onAction = viewModel::onAction ) } @Composable -private fun ConsentScreen( +internal fun OnboardingConsentComposableContent( + markdown: suspend () -> ByteArray, + action: suspend () -> Unit, + title: StringResource?, + identifier: String, + exportConfiguration: ConsentDocumentExportConfiguration, uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit, + onAction: (ConsentAction) -> Unit ) { - Column( + val actionScope = rememberCoroutineScope() + OnboardingComposable( modifier = Modifier .testIdentifier(ConsentScreenTestIdentifier.ROOT) - .fillMaxSize() - .padding(Spacings.medium) - ) { + .fillMaxSize(), + title = { + title?.let { + OnboardingTitle(it) + } + }, + content = { + ConsentDocument( + markdown = markdown, + viewState = remember { mutableStateOf(ConsentViewState.Base(ViewState.Idle)) }, + exportConfiguration = exportConfiguration, + ).Composable( + modifier = Modifier.padding(bottom = Spacings.medium), + uiState = uiState, + onAction = onAction, + ) + }, + action = { + Button( + onClick = { + actionScope.launch { + onAction(ConsentAction.Consent(identifier, exportConfiguration)) + action() + } + }, + enabled = uiState.isValidForm, + modifier = Modifier.fillMaxWidth(1f) + ) { + Text("I Consent") + } + } + ) + Column { Spacer(modifier = Modifier.height(Spacings.medium)) - MarkdownComponent(markdownElements = uiState.markdownElements) + MarkdownComposable(markdown) Spacer( modifier = Modifier .height(Spacings.small) .weight(1f) ) - SignaturePad( - uiState = uiState, - onAction = onAction, - ) } } @@ -69,7 +118,15 @@ private fun ConsentScreenPreview( @PreviewParameter(ConsentScreenPreviewProvider::class) uiState: ConsentUiState, ) { SpeziTheme { - ConsentScreen(uiState = uiState, onAction = { }) + OnboardingConsentComposableContent( + markdown = { ByteArray(0) }, + action = {}, + title = null, + identifier = "ConsentDocument", + exportConfiguration = ConsentDocumentExportConfiguration(), + uiState = uiState, + onAction = {} + ) } } @@ -77,16 +134,14 @@ private fun ConsentScreenPreview( private class ConsentScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( ConsentUiState( - firstName = FieldState("John"), - lastName = FieldState("Doe"), + name = PersonNameComponents(givenName = "John", familyName = "Doe"), paths = mutableListOf(Path().apply { lineTo(100f, 100f) }), markdownElements = listOf( MarkdownElement.Heading(1, "Consent"), MarkdownElement.Paragraph("Please sign below to indicate your consent."), ), ), ConsentUiState( - firstName = FieldState(""), - lastName = FieldState(""), + name = PersonNameComponents(givenName = "", familyName = ""), paths = mutableListOf(), ) ) diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt deleted file mode 100644 index 1b6e7e6b3..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/SignaturePad.kt +++ /dev/null @@ -1,138 +0,0 @@ -@file:Suppress("MagicNumber") - -package edu.stanford.spezi.module.onboarding.consent - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.Button -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import edu.stanford.spezi.core.design.theme.Spacings - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -internal fun SignaturePad( - uiState: ConsentUiState, - onAction: (ConsentAction) -> Unit, -) { - val keyboardController = LocalSoftwareKeyboardController.current - Column { - OutlinedTextField( - value = uiState.firstName.value, - onValueChange = { - onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.FIRST_NAME)) - }, - modifier = Modifier.fillMaxWidth(), - label = { Text("First Name") }, - singleLine = true, - isError = uiState.firstName.error, - trailingIcon = { Icon(Icons.Filled.Info, contentDescription = "Information Icon") } - ) - Spacer(modifier = Modifier.height(Spacings.small)) - OutlinedTextField( - value = uiState.lastName.value, - onValueChange = { - onAction(ConsentAction.TextFieldUpdate(it, TextFieldType.LAST_NAME)) - }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Last Name") }, - isError = uiState.lastName.error, - singleLine = true, - trailingIcon = { - Icon( - Icons.Filled.Info, - contentDescription = "Information Icon" - ) - } - ) - - if (uiState.firstName.value.isNotBlank() && uiState.lastName.value.isNotBlank()) { - Spacer(modifier = Modifier.height(Spacings.medium)) - Text("Signature:") - SignatureCanvas( - paths = uiState.paths.toMutableList(), - firstName = uiState.firstName.value, - lastName = uiState.lastName.value, - onPathAdd = { path -> - onAction(ConsentAction.AddPath(path)) - keyboardController?.hide() - } - ) - Spacer(modifier = Modifier.height(Spacings.medium)) - Row(modifier = Modifier.fillMaxWidth()) { - FilledTonalButton( - onClick = { - if (uiState.paths.isNotEmpty()) { - onAction(ConsentAction.Undo) - } - }, - enabled = uiState.paths.isNotEmpty(), - modifier = Modifier.weight(1f), - ) { - Text("Undo") - } - Spacer(modifier = Modifier.width(Spacings.medium)) - Button( - onClick = { - onAction(ConsentAction.Consent) - }, - enabled = uiState.isValidForm, - modifier = Modifier.weight(1f) - ) { - Text("I Consent") - } - } - } - } -} - -@Preview -@Composable -private fun SignaturePadPreview( - @PreviewParameter(SignaturePadPreviewProvider::class) data: SignaturePadPreviewData, -) { - SignaturePad( - uiState = ConsentUiState( - firstName = FieldState(data.firstName), - lastName = FieldState(data.lastName), - paths = data.paths - ) - ) {} -} - -private data class SignaturePadPreviewData( - val paths: MutableList, - val firstName: String, - val lastName: String, -) - -private class SignaturePadPreviewProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - SignaturePadPreviewData( - paths = mutableListOf(Path()), - firstName = "", - lastName = "" - ), - SignaturePadPreviewData( - paths = mutableListOf(Path().apply { lineTo(100f, 100f) }.apply { lineTo(250f, 200f) }), - firstName = "Jane", - lastName = "Doe" - ) - ) -} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt deleted file mode 100644 index 2253fc5ca..000000000 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/consent/ViewState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package edu.stanford.spezi.module.onboarding.consent - -sealed interface ViewState { - data object Idle : ViewState - data object Processing : ViewState - data class Error(val throwable: Throwable?) : ViewState -} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt new file mode 100644 index 000000000..9279176eb --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingActions.kt @@ -0,0 +1,48 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import edu.stanford.spezi.core.design.component.StringResource +import edu.stanford.spezi.module.onboarding.views.SuspendButton +import edu.stanford.spezi.module.onboarding.views.ViewState +import edu.stanford.spezi.module.onboarding.views.ViewStateAlert + +@Composable +fun OnboardingActions( + primaryText: StringResource, + primaryAction: suspend () -> Unit, + secondaryText: StringResource? = null, + secondaryAction: (suspend () -> Unit)? = null +) { + val primaryActionState = remember { mutableStateOf(ViewState.Idle) } + val secondaryActionState = remember { mutableStateOf(ViewState.Idle) } + + ViewStateAlert(primaryActionState) + ViewStateAlert(secondaryActionState) + + Column(Modifier.padding(top = 10.dp)) { + SuspendButton(primaryActionState, primaryAction) { + Text( + primaryText.text(), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 38.dp) + ) + } + secondaryText?.let { secondaryText -> + secondaryAction?.let { secondaryAction -> + SuspendButton(secondaryActionState, secondaryAction) { + Text(secondaryText.text()) + } + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt new file mode 100644 index 000000000..f40a698d8 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingComposable.kt @@ -0,0 +1,43 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + + +@Composable +fun OnboardingComposable( + modifier: Modifier = Modifier, + title: @Composable () -> Unit = {}, + content: @Composable () -> Unit, + action: (@Composable () -> Unit)? = null, +) { + val size = remember { mutableStateOf(IntSize.Zero) } + Box(modifier.onSizeChanged { size.value = it }) { + LazyColumn { + item { + Column(Modifier.heightIn(min = size.value.height.dp)) { + Column { + title() + content() + } + action?.let { action -> + Spacer(Modifier) + action() + } + Spacer(Modifier.height(10.dp)) + } + } + } + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt index 4d1d6a98a..4a79dd02d 100644 --- a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingStack.kt @@ -1,4 +1 @@ package edu.stanford.spezi.module.onboarding.onboarding - -class OnboardingStack { -} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt new file mode 100644 index 000000000..4a775e512 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/OnboardingTitle.kt @@ -0,0 +1,37 @@ +package edu.stanford.spezi.module.onboarding.onboarding + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import edu.stanford.spezi.core.design.component.StringResource + +@Composable +fun OnboardingTitle(title: StringResource, subtitle: StringResource? = null) { + OnboardingTitle(title.text(), subtitle?.text()) +} + +@Composable +fun OnboardingTitle(title: String, subtitle: String? = null) { + Column(Modifier.padding(vertical = 8.dp)) { + Text( + title, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + textAlign = TextAlign.Center + ) + + subtitle?.let { subtitle -> + Text( + subtitle, + modifier = Modifier.padding(bottom = 8.dp), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt new file mode 100644 index 000000000..14f99b9f8 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/onboarding/flow/IllegalOnboardingStepComposable.kt @@ -0,0 +1,10 @@ +package edu.stanford.spezi.module.onboarding.onboarding.flow + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import edu.stanford.spezi.core.design.component.StringResource + +@Composable +internal fun IllegalOnboardingStepComposable() { + Text(StringResource("Illegal onboarding step").text()) +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt new file mode 100644 index 000000000..bb196a0a2 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/Standard.kt @@ -0,0 +1,3 @@ +package edu.stanford.spezi.module.onboarding.views + +interface Standard diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt new file mode 100644 index 000000000..52bce60cb --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/SuspendButton.kt @@ -0,0 +1,61 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import edu.stanford.spezi.core.design.component.Button +import edu.stanford.spezi.core.utils.UUID +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +private enum class SuspendButtonState { + IDLE, DISABLED, DISABLED_AND_PROCESSING; +} + +@Composable +fun SuspendButton( + state: MutableState, + action: suspend () -> Unit, + label: @Composable () -> Unit +) { + val buttonState = remember { mutableStateOf(SuspendButtonState.IDLE) } + val coroutineScope = rememberCoroutineScope() + val debounceScope = rememberCoroutineScope() + + DisposableEffect(remember { UUID() }) { + onDispose { + coroutineScope.cancel() + } + } + + Button( + onClick = { + if (state.value == ViewState.Processing) return@Button + buttonState.value = SuspendButtonState.DISABLED + + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Processing + + coroutineScope.launch { + runCatching { + action() + if (state.value != ViewState.Idle) { + // TODO: iOS animates this assignment specifically - is this possible in Jetpack Compose? + state.value = ViewState.Idle + } + }.onFailure { + state.value = ViewState.Error(it) + } + + buttonState.value = SuspendButtonState.IDLE + } + }, + enabled = !coroutineScope.isActive + ) { + label() + } +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt new file mode 100644 index 000000000..787b05b31 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewState.kt @@ -0,0 +1,17 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import edu.stanford.spezi.core.design.component.StringResource + +sealed interface ViewState { + data object Idle : ViewState + data object Processing : ViewState + data class Error(val throwable: Throwable?) : ViewState + + val errorTitle: String + @Composable @ReadOnlyComposable get() = StringResource("Error").text() + + val errorDescription: String + @Composable @ReadOnlyComposable get() = if (this is Error) throwable?.localizedMessage ?: "" else "" +} \ No newline at end of file diff --git a/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt new file mode 100644 index 000000000..df758c582 --- /dev/null +++ b/modules/onboarding/src/main/kotlin/edu/stanford/spezi/module/onboarding/views/ViewStateAlert.kt @@ -0,0 +1,33 @@ +package edu.stanford.spezi.module.onboarding.views + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState + +@Composable +fun ViewStateAlert(state: MutableState) { + if (state.value is ViewState.Error) { + AlertDialog( + title = { + Text(text = state.value.errorTitle) + }, + text = { + Text(text = state.value.errorDescription) + }, + onDismissRequest = { + state.value = ViewState.Idle + }, + confirmButton = { + TextButton( + onClick = { + state.value = ViewState.Idle + } + ) { + Text("Okay") + } + } + ) + } +} \ No newline at end of file