From 8a34cf4cbf05558acaaa88110729777736d17dc7 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 18:56:11 +0200 Subject: [PATCH 1/8] add additional icons Signed-off-by: Basler182 --- core/design/src/main/res/drawable/ic_assignment.xml | 11 +++++++++++ core/design/src/main/res/drawable/ic_google.xml | 9 +++++++++ core/design/src/main/res/drawable/ic_groups.xml | 10 ++++++++++ core/design/src/main/res/drawable/ic_medication.xml | 4 ++-- core/design/src/main/res/drawable/ic_vital_signs.xml | 10 ++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 core/design/src/main/res/drawable/ic_assignment.xml create mode 100644 core/design/src/main/res/drawable/ic_google.xml create mode 100644 core/design/src/main/res/drawable/ic_groups.xml create mode 100644 core/design/src/main/res/drawable/ic_vital_signs.xml diff --git a/core/design/src/main/res/drawable/ic_assignment.xml b/core/design/src/main/res/drawable/ic_assignment.xml new file mode 100644 index 000000000..994243fc1 --- /dev/null +++ b/core/design/src/main/res/drawable/ic_assignment.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/design/src/main/res/drawable/ic_google.xml b/core/design/src/main/res/drawable/ic_google.xml new file mode 100644 index 000000000..276377df1 --- /dev/null +++ b/core/design/src/main/res/drawable/ic_google.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/core/design/src/main/res/drawable/ic_groups.xml b/core/design/src/main/res/drawable/ic_groups.xml new file mode 100644 index 000000000..1b86958aa --- /dev/null +++ b/core/design/src/main/res/drawable/ic_groups.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/core/design/src/main/res/drawable/ic_medication.xml b/core/design/src/main/res/drawable/ic_medication.xml index 7b4268d76..df30c3a1a 100644 --- a/core/design/src/main/res/drawable/ic_medication.xml +++ b/core/design/src/main/res/drawable/ic_medication.xml @@ -2,9 +2,9 @@ xmlns:tools="http://schemas.android.com/tools" android:width="24dp" android:height="24dp" - android:alpha="0.6" - android:tint="#333333" android:viewportWidth="960" + android:alpha="1" + android:tint="#333333" android:viewportHeight="960"> + + \ No newline at end of file From 1cd317722147bbfc86d5108b86bca5519b2716e6 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 19:06:44 +0200 Subject: [PATCH 2/8] feat SpeziValidatedOutlinedTextField Signed-off-by: Basler182 --- .../SpeziValidatedOutlinedTextField.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 core/design/src/main/java/edu/stanford/spezikt/core/design/component/SpeziValidatedOutlinedTextField.kt diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/component/SpeziValidatedOutlinedTextField.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/component/SpeziValidatedOutlinedTextField.kt new file mode 100644 index 000000000..eb1904058 --- /dev/null +++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/component/SpeziValidatedOutlinedTextField.kt @@ -0,0 +1,96 @@ +package edu.stanford.spezikt.core.design.component + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import edu.stanford.spezikt.core.design.theme.SpeziTheme + +@Composable +fun SpeziValidatedOutlinedTextField( + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + value: String, + labelText: String, + errorText: String?, + isValid: Boolean +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + modifier = modifier.fillMaxWidth(), + value = value, + onValueChange = { + onValueChange(it) + }, + label = { Text(labelText) }, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next), + isError = !isValid, + ) + if (!isValid) { + Text( + text = errorText ?: "", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error + ) + } + } +} + +@Preview( + name = "Light Mode", + uiMode = Configuration.UI_MODE_NIGHT_NO +) +@Preview( + name = "Dark Mode", + uiMode = Configuration.UI_MODE_NIGHT_YES +) +@Composable +fun SpeziValidatedOutlinedTextFieldPreview( + @PreviewParameter(SpeziValidatedOutlinedTextFieldProvider::class) params: SpeziValidatedOutlinedTextFieldParams +) { + SpeziTheme { + SpeziValidatedOutlinedTextField( + onValueChange = {}, + value = params.value, + labelText = params.labelText, + errorText = params.errorText, + isValid = params.isValid + ) + } +} + +class SpeziValidatedOutlinedTextFieldProvider : + PreviewParameterProvider { + override val values = sequenceOf( + SpeziValidatedOutlinedTextFieldParams( + value = "", + labelText = "Label", + errorText = "The input is invalid", + isValid = false + ), + SpeziValidatedOutlinedTextFieldParams( + value = "", + labelText = "Label", + errorText = "", + isValid = true + ) + ) +} + +data class SpeziValidatedOutlinedTextFieldParams( + val value: String, + val labelText: String, + val errorText: String, + val isValid: Boolean +) \ No newline at end of file From dbbf4ef82f21b7990effac3ed2a0c534b1da1096 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 19:10:42 +0200 Subject: [PATCH 3/8] add network configuration for emulators Signed-off-by: Basler182 --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9adaf0bb2..2ec9fa7b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SpeziKt"> diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..293a11c03 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,5 @@ + + + 10.0.2.2 + + \ No newline at end of file From f5964c6272804be6f2cb925ea977672556f29703 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 19:39:05 +0200 Subject: [PATCH 4/8] feat onboarding screen Signed-off-by: Basler182 --- app/build.gradle.kts | 2 + .../onboarding/DefaultOnboardingRepository.kt | 56 ++++++++++ .../spezikt/onboarding/OnboardingModule.kt | 23 ++++ build.gradle.kts | 1 + spezi-module/onboarding/build.gradle.kts | 14 +++ .../onboarding/OnboardingScreen.kt | 20 ---- .../spezikt/spezi_module/onboarding/README.MD | 2 + .../onboarding/onboarding/Area.kt | 13 +++ .../onboarding/OnboardingRepository.kt | 26 +++++ .../onboarding/onboarding/OnboardingScreen.kt | 104 ++++++++++++++++++ .../onboarding/OnboardingUiState.kt | 23 ++++ .../onboarding/OnboardingViewModel.kt | 68 ++++++++++++ 12 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/edu/stanford/spezikt/onboarding/DefaultOnboardingRepository.kt create mode 100644 app/src/main/java/edu/stanford/spezikt/onboarding/OnboardingModule.kt delete mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/README.MD create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/Area.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingRepository.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingScreen.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingUiState.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 78b0a9916..b0789285e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,4 +41,6 @@ dependencies { implementation(libs.hilt.navigation.compose) implementation(project(":core:bluetooth")) + implementation(project(":spezi-module:onboarding")) + implementation(project(":core:coroutines")) } \ No newline at end of file diff --git a/app/src/main/java/edu/stanford/spezikt/onboarding/DefaultOnboardingRepository.kt b/app/src/main/java/edu/stanford/spezikt/onboarding/DefaultOnboardingRepository.kt new file mode 100644 index 000000000..c16cf80c3 --- /dev/null +++ b/app/src/main/java/edu/stanford/spezikt/onboarding/DefaultOnboardingRepository.kt @@ -0,0 +1,56 @@ +package edu.stanford.spezikt.onboarding + +import edu.stanford.spezikt.core.design.R +import edu.stanford.spezikt.coroutines.di.Dispatching +import edu.stanford.spezikt.spezi_module.onboarding.onboarding.Area +import edu.stanford.spezikt.spezi_module.onboarding.onboarding.OnboardingRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class DefaultOnboardingRepository @Inject constructor( + @Dispatching.IO private val scope: CoroutineScope, +) : OnboardingRepository { + + override suspend fun getAreas(): Result> = withContext(scope.coroutineContext) { + try { + Result.success( + listOf( + Area( + title = "Join the Study", + iconId = R.drawable.ic_groups, + description = "Connect to your study via an invitation code from the researchers." + ), + Area( + title = "Complete Health Checks", + iconId = R.drawable.ic_assignment, + description = "Record and report health data automatically according to a schedule set by the research team." + ), + Area( + title = "Visualize Data", + iconId = R.drawable.ic_vital_signs, + description = "Visualize your heart health progress throughout participation in the study." + ) + ) + ) + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getTitle(): Result = withContext(scope.coroutineContext) { + try { + Result.success("Welcome to ENGAGE-HF") + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getSubtitle(): Result = withContext(scope.coroutineContext) { + try { + Result.success("Remote study participation made easy.") + } catch (e: Exception) { + Result.failure(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/edu/stanford/spezikt/onboarding/OnboardingModule.kt b/app/src/main/java/edu/stanford/spezikt/onboarding/OnboardingModule.kt new file mode 100644 index 000000000..7c18aa9ca --- /dev/null +++ b/app/src/main/java/edu/stanford/spezikt/onboarding/OnboardingModule.kt @@ -0,0 +1,23 @@ +package edu.stanford.spezikt.onboarding + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import edu.stanford.spezikt.coroutines.di.Dispatching +import edu.stanford.spezikt.spezi_module.onboarding.onboarding.OnboardingRepository +import kotlinx.coroutines.CoroutineScope +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object OnboardingModule { + + @Provides + @Singleton + fun provideOnboardingRepository( + @Dispatching.IO ioCoroutineScope: CoroutineScope + ): OnboardingRepository { + return DefaultOnboardingRepository(ioCoroutineScope) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 863c38ce8..e67759788 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.hilt.android) version libs.versions.hiltVersion apply false alias(libs.plugins.google.devtools.ksp) version libs.versions.kspVersion apply false alias(libs.plugins.dokka) version libs.versions.dokka + alias(libs.plugins.google.gms.google.services) apply false } subprojects { diff --git a/spezi-module/onboarding/build.gradle.kts b/spezi-module/onboarding/build.gradle.kts index 369a67f2b..65c8400d8 100644 --- a/spezi-module/onboarding/build.gradle.kts +++ b/spezi-module/onboarding/build.gradle.kts @@ -1,8 +1,22 @@ plugins { alias(libs.plugins.spezikt.library) alias(libs.plugins.spezikt.compose) + alias(libs.plugins.spezikt.hilt) + alias(libs.plugins.google.gms.google.services) } android { namespace = "edu.stanford.spezikt.spezi_module.onboarding" +} + +dependencies { + implementation(libs.firebase.functions.ktx) + implementation(libs.hilt.navigation.compose) + implementation(libs.firebase.auth) + + implementation(project(":core:coroutines")) + + testImplementation(libs.bundles.unit.testing) + + androidTestImplementation(libs.bundles.compose.androidTest) } \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt deleted file mode 100644 index 875cb54d1..000000000 --- a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package edu.stanford.spezikt.spezi_module.onboarding - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.tooling.preview.Preview -import edu.stanford.spezikt.core.design.theme.SpeziTheme - -@Composable -fun OnboardingScreen() { - Text(text = "Onboarding Screen") -} - - -@Preview -@Composable -fun OnboardingScreenPreview() { - SpeziTheme { - OnboardingScreen() - } -} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/README.MD b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/README.MD new file mode 100644 index 000000000..a2abc9d03 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/README.MD @@ -0,0 +1,2 @@ +# Module onboarding + diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/Area.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/Area.kt new file mode 100644 index 000000000..101e9deaf --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/Area.kt @@ -0,0 +1,13 @@ +package edu.stanford.spezikt.spezi_module.onboarding.onboarding + +/** + * Represents an area of the onboarding screen. + * @property title The title of the area. + * @property iconId The resource ID for the icon. + * @property description The description of the area. + */ +data class Area( + val title: String, + val iconId: Int, // Resource ID for the icon + val description: String +) diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingRepository.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingRepository.kt new file mode 100644 index 000000000..e2089fb2a --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingRepository.kt @@ -0,0 +1,26 @@ +package edu.stanford.spezikt.spezi_module.onboarding.onboarding + +/** + * Repository for fetching onboarding data. + */ +interface OnboardingRepository { + + /** + * Fetches the areas of the onboarding screen. + * @return A list of [Area] objects. + * @see Area + */ + suspend fun getAreas(): Result> + + /** + * Fetches the title of the onboarding screen. + * @return A string representing the title. + */ + suspend fun getTitle(): Result + + /** + * Fetches the subtitle of the onboarding screen. + * @return A string representing the subtitle. + */ + suspend fun getSubtitle(): Result +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingScreen.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingScreen.kt new file mode 100644 index 000000000..22f452de0 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingScreen.kt @@ -0,0 +1,104 @@ +package edu.stanford.spezikt.spezi_module.onboarding.onboarding + + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezikt.core.design.theme.Colors.primary +import edu.stanford.spezikt.core.design.theme.Sizes +import edu.stanford.spezikt.core.design.theme.Spacings +import edu.stanford.spezikt.core.design.theme.TextStyles.bodyLarge +import edu.stanford.spezikt.core.design.theme.TextStyles.bodyMedium +import edu.stanford.spezikt.core.design.theme.TextStyles.titleLarge +import edu.stanford.spezikt.core.design.theme.TextStyles.titleSmall + + +/** + * The onboarding screen. + */ +@Composable +fun OnboardingScreen( +) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(Spacings.medium), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + verticalArrangement = Arrangement.spacedBy(Spacings.medium) + ) { + Text( + text = uiState.title, + style = titleLarge + ) + + Text(text = uiState.subtitle, style = bodyLarge) + Spacer(modifier = Modifier.height(Spacings.small)) + + LazyColumn { + items(uiState.areas.size) { index -> + FeatureItem(area = uiState.areas[index]) + Spacer(modifier = Modifier.height(Spacings.medium)) + } + } + } + Button( + onClick = { viewModel.onAction(Action.OnLearnMoreClicked) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Learn More") + } + } +} + + +@Composable +fun FeatureItem(area: Area) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Spacings.small), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = area.iconId), + contentDescription = "Area Icon", + modifier = Modifier.size(Sizes.icon), + tint = primary + ) + Spacer(Modifier.width(Spacings.medium)) + Column { + Text( + text = area.title, + style = titleSmall + ) + Text( + text = area.description, + style = bodyMedium + ) + } + } +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingUiState.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingUiState.kt new file mode 100644 index 000000000..149f11dc3 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingUiState.kt @@ -0,0 +1,23 @@ +package edu.stanford.spezikt.spezi_module.onboarding.onboarding + +/** + * A sealed class representing the actions that can be performed on the onboarding screen. + */ +sealed class Action { + data class UpdateArea(val areas: List) : Action() + + data object ClearError : Action() + + data object OnLearnMoreClicked : Action() + +} + +/** + * The UI state for the onboarding screen. + */ +data class OnboardingUiState( + val areas: List = emptyList(), + val title: String = "Title", + val subtitle: String = "Subtitle", + val error: String? = null, +) \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingViewModel.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingViewModel.kt new file mode 100644 index 000000000..3e940c311 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/onboarding/OnboardingViewModel.kt @@ -0,0 +1,68 @@ +package edu.stanford.spezikt.spezi_module.onboarding.onboarding + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import edu.stanford.spezikt.coroutines.di.Dispatching +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OnboardingViewModel @Inject internal constructor( + private val repository: OnboardingRepository, + @Dispatching.IO private val scope: CoroutineScope, +) : ViewModel() { + + private val _uiState = MutableStateFlow(OnboardingUiState()) + val uiState = _uiState.asStateFlow() + + init { + init() + } + + fun onAction(action: Action) { + _uiState.update { + when (action) { + is Action.UpdateArea -> { + val newAreas = action.areas + it.copy(areas = newAreas) + } + + Action.OnLearnMoreClicked -> TODO() + Action.ClearError -> it.copy(error = null) + } + } + } + + private fun init() { + scope.launch { + val result = repository.getAreas() + if (result.isSuccess) { + _uiState.update { + it.copy(areas = result.getOrNull() ?: emptyList()) + } + } else { + _uiState.update { + it.copy(error = "Failed to load areas") + } + } + + val title = repository.getTitle() + if (title.isSuccess) { + _uiState.update { + it.copy(title = title.getOrNull() ?: "") + } + } + + val subTitle = repository.getSubtitle() + if (title.isSuccess) { + _uiState.update { + it.copy(subtitle = subTitle.getOrNull() ?: "") + } + } + } + } +} \ No newline at end of file From dc3a01dbea5c9682f2c91ed05c823f1bdcecef51 Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 20:23:51 +0200 Subject: [PATCH 5/8] feat InvitationCode Signed-off-by: Basler182 --- .../invitation/FirebaseAuthManager.kt | 59 +++++++++++++ .../invitation/InvitationCodeScreen.kt | 88 +++++++++++++++++++ .../invitation/InvitationCodeUiState.kt | 16 ++++ .../invitation/InvitationCodeViewModel.kt | 50 +++++++++++ 4 files changed, 213 insertions(+) create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeScreen.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeUiState.kt create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt new file mode 100644 index 000000000..492e8493c --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt @@ -0,0 +1,59 @@ +package edu.stanford.spezikt.spezi_module.onboarding.invitation + +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.functions.FirebaseFunctions +import edu.stanford.spezi.logging.speziLogger +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import kotlin.coroutines.resumeWithException + +class FirebaseAuthManager @Inject constructor() { + + private val logger by speziLogger() + + private val functions: FirebaseFunctions by lazy { + val instance = FirebaseFunctions.getInstance() + instance.useEmulator("10.0.2.2", 5001) + instance + } + + private val auth: FirebaseAuth by lazy { + val instance = FirebaseAuth.getInstance() + instance.useEmulator("10.0.2.2", 9099) + instance + } + + suspend fun checkInvitationCode(invitationCode: String): Result { + return try { + auth.signOut() + val authResult = auth.signInAnonymously().await() + val userId = authResult.user?.uid + + val data = hashMapOf( + "invitationCode" to invitationCode, + "userId" to userId + ) + logger.i { "Checking invitation code: $data" } + + functions + .getHttpsCallable("checkInvitationCode") + .call(data) + .await() + + logger.i { "Successfully checked invitation code" } + Result.success(Unit) + } catch (e: Exception) { + logger.e { "Failed to check invitation code: ${e.message}" } + Result.failure(e) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun com.google.android.gms.tasks.Task.await(): T { + return suspendCancellableCoroutine { cont -> + addOnSuccessListener { result -> cont.resume(result) { } } + addOnFailureListener { exception -> cont.resumeWithException(exception) } + } + } +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeScreen.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeScreen.kt new file mode 100644 index 000000000..755202323 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeScreen.kt @@ -0,0 +1,88 @@ +package edu.stanford.spezikt.spezi_module.onboarding.invitation + +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.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import edu.stanford.spezikt.core.design.component.SpeziValidatedOutlinedTextField +import edu.stanford.spezikt.core.design.theme.Colors.onPrimary +import edu.stanford.spezikt.core.design.theme.Colors.primary +import edu.stanford.spezikt.core.design.theme.Sizes +import edu.stanford.spezikt.core.design.theme.Spacings +import edu.stanford.spezikt.core.design.theme.TextStyles.titleLarge + +@Composable +fun InvitationCodeScreen( +) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(Spacings.medium), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Invitation Code", + style = titleLarge, + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Icon( + imageVector = Icons.Default.Edit, + tint = primary, + contentDescription = "Edit Icon", + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(Sizes.icon) + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Text("Please enter your invitation code to join the ENGAGE-HF study.") + Spacer(modifier = Modifier.height(Spacings.medium)) + SpeziValidatedOutlinedTextField( + value = uiState.invitationCode, + onValueChange = { + viewModel.onAction( + Action.UpdateInvitationCode( + it, + TextFieldType.INVITATION_CODE + ) + ) + viewModel.onAction(Action.ClearError) + }, + labelText = "Invitation Code", + errorText = uiState.error, + isValid = uiState.error == null + ) + Spacer(modifier = Modifier.height(Spacings.medium)) + Button( + onClick = { + viewModel.redeemInvitationCode() + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Redeem Invitation Code", color = onPrimary) + } + Spacer(modifier = Modifier.height(Spacings.small)) + TextButton(onClick = { + // TODO navigate to login screen + }) { + Text("I Already Have an Account") + } + } +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeUiState.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeUiState.kt new file mode 100644 index 000000000..4dc73588b --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeUiState.kt @@ -0,0 +1,16 @@ +package edu.stanford.spezikt.spezi_module.onboarding.invitation + +data class InvitationCodeUiState( + val invitationCode: String = "", + val error: String? = "" +) + +enum class TextFieldType { + INVITATION_CODE +} + +sealed interface Action { + data class UpdateInvitationCode(val invitationCode: String, val type: TextFieldType) : Action + + data object ClearError : Action +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt new file mode 100644 index 000000000..7765967fd --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt @@ -0,0 +1,50 @@ +package edu.stanford.spezikt.spezi_module.onboarding.invitation + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import edu.stanford.spezikt.coroutines.di.Dispatching +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class InvitationCodeViewModel @Inject internal constructor( + private val fam: FirebaseAuthManager, + @Dispatching.IO private val scope: CoroutineScope, +) : ViewModel() { + + private val _uiState = + MutableStateFlow(InvitationCodeUiState(invitationCode = "", error = null)) + val uiState = _uiState.asStateFlow() + + fun onAction(action: Action) { + _uiState.update { + when (action) { + is Action.UpdateInvitationCode -> { + val newValue = action.invitationCode + it.copy(invitationCode = newValue) + } + + is Action.ClearError -> { + it.copy(error = null) + } + } + } + } + + fun redeemInvitationCode() { + scope.launch { + val result = fam.checkInvitationCode(uiState.value.invitationCode) + if (result.isSuccess) { + // TODO navigate to login or register screen + } else { + _uiState.update { + it.copy(error = "Invitation Code is already used or incorrect") + } + } + } + } +} \ No newline at end of file From 997fa5b250b5c98bcf7552b7e143fd40dec5877b Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 20:38:53 +0200 Subject: [PATCH 6/8] update libs.versions.toml add firestore, functions, storage, services Signed-off-by: Basler182 --- gradle/libs.versions.toml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0629f25c9..fed033efd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,11 @@ coreKtx = "1.13.0" coroutinesVersion = "1.6.4" dokka = "1.9.20" espressoCore = "3.5.1" +firebaseAuth = "23.0.0" +firebaseFirestoreKtx = "25.0.0" +firebaseFunctionsKtx = "21.0.0" +firebaseStorageKtx = "21.0.0" +googleGmsGoogleServices = "4.4.1" hiltNavigation = "1.2.0" hiltVersion = "2.51" junit = "4.13.2" @@ -24,7 +29,7 @@ targetSdk = "34" timberVersion = "5.0.1" truth = "1.4.2" -# Please keep [libraries] block sorted. Select all items and in Android Studio `File > Sort Lines` +# Please keep [libraries] block sorted. Select all items and in Android Studio `Edit > Sort Lines` [libraries] android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" } @@ -47,6 +52,10 @@ compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutinesVersion" } coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutinesVersion" } coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesVersion" } +firebase-auth = { group = "com.google.firebase", name = "firebase-auth", version.ref = "firebaseAuth" } +firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebaseFirestoreKtx" } +firebase-functions-ktx = { group = "com.google.firebase", name = "firebase-functions-ktx", version.ref = "firebaseFunctionsKtx" } +firebase-storage-ktx = { group = "com.google.firebase", name = "firebase-storage-ktx", version.ref = "firebaseStorageKtx" } google-truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltVersion" } hilt-core = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltVersion" } @@ -62,12 +71,13 @@ mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "moc mockk-core = { group = "io.mockk", name = "mockk", version.ref = "mockKVersion" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timberVersion" } -# Please keep [plugins] block sorted. Select all items and in Android Studio `File > Sort Lines` +# Please keep [plugins] block sorted. Select all items and in Android Studio `Edit > Sort Lines` [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" } +google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltVersion" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } spezikt-application = { id = "spezikt.application", version = "unspecified" } @@ -76,7 +86,7 @@ spezikt-compose = { id = "spezikt.compose", version = "unspecified" } spezikt-hilt = { id = "spezikt.hilt", version = "unspecified" } spezikt-library = { id = "spezikt.library", version = "unspecified" } -# Please keep [bundles] block sorted. Select all items and in Android Studio `File > Sort Lines` +# Please keep [bundles] block sorted. Select all items and in Android Studio `Edit > Sort Lines` [bundles] compose = ["compose-ui", "compose-material3", "compose-ui-tooling-preview", "compose-ui-tooling", "androidx-core-ktx", "androidx-appcompat", "androidx-activity-compose"] compose-androidTest = ["junit", "androidx-espresso-core", "compose-ui-test", "mockk-agent-core", "mockk-android", "google-truth"] From c06b298388a0f3ce0437c0a59197b990199dffff Mon Sep 17 00:00:00 2001 From: Basler182 Date: Mon, 20 May 2024 20:41:32 +0200 Subject: [PATCH 7/8] add google services to app Signed-off-by: Basler182 --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b0789285e..f7912878f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.spezikt.application) alias(libs.plugins.spezikt.compose) alias(libs.plugins.spezikt.hilt) + alias(libs.plugins.google.gms.google.services) } android { From 6f328f59593749a897280927318133ebf4a2e65b Mon Sep 17 00:00:00 2001 From: Basler182 Date: Tue, 21 May 2024 17:08:22 +0200 Subject: [PATCH 8/8] add interface layer to invitation Signed-off-by: Basler182 --- .../onboarding/di/OnboardingModule.kt | 18 ++++++++++++++++++ ...ger.kt => FirebaseInvitationAuthManager.kt} | 5 +++-- .../invitation/InvitationAuthManager.kt | 5 +++++ .../invitation/InvitationCodeViewModel.kt | 4 ++-- 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/di/OnboardingModule.kt rename spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/{FirebaseAuthManager.kt => FirebaseInvitationAuthManager.kt} (91%) create mode 100644 spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationAuthManager.kt diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/di/OnboardingModule.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/di/OnboardingModule.kt new file mode 100644 index 000000000..f6afbc14c --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/di/OnboardingModule.kt @@ -0,0 +1,18 @@ +package edu.stanford.spezikt.spezi_module.onboarding.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import edu.stanford.spezikt.spezi_module.onboarding.invitation.FirebaseInvitationAuthManager +import edu.stanford.spezikt.spezi_module.onboarding.invitation.InvitationAuthManager + +@Module +@InstallIn(SingletonComponent::class) +object OnboardingModule { + + @Provides + fun provideInvitationAuthManager(): InvitationAuthManager { + return FirebaseInvitationAuthManager() + } +} \ No newline at end of file diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseInvitationAuthManager.kt similarity index 91% rename from spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt rename to spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseInvitationAuthManager.kt index 492e8493c..01098c81f 100644 --- a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseAuthManager.kt +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/FirebaseInvitationAuthManager.kt @@ -8,7 +8,8 @@ import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject import kotlin.coroutines.resumeWithException -class FirebaseAuthManager @Inject constructor() { + +class FirebaseInvitationAuthManager @Inject constructor() : InvitationAuthManager { private val logger by speziLogger() @@ -24,7 +25,7 @@ class FirebaseAuthManager @Inject constructor() { instance } - suspend fun checkInvitationCode(invitationCode: String): Result { + override suspend fun checkInvitationCode(invitationCode: String): Result { return try { auth.signOut() val authResult = auth.signInAnonymously().await() diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationAuthManager.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationAuthManager.kt new file mode 100644 index 000000000..f08a20381 --- /dev/null +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationAuthManager.kt @@ -0,0 +1,5 @@ +package edu.stanford.spezikt.spezi_module.onboarding.invitation + +interface InvitationAuthManager { + suspend fun checkInvitationCode(invitationCode: String): Result +} diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt index 7765967fd..681e8b6d2 100644 --- a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt +++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/invitation/InvitationCodeViewModel.kt @@ -12,7 +12,7 @@ import javax.inject.Inject @HiltViewModel class InvitationCodeViewModel @Inject internal constructor( - private val fam: FirebaseAuthManager, + private val iam: InvitationAuthManager, @Dispatching.IO private val scope: CoroutineScope, ) : ViewModel() { @@ -37,7 +37,7 @@ class InvitationCodeViewModel @Inject internal constructor( fun redeemInvitationCode() { scope.launch { - val result = fam.checkInvitationCode(uiState.value.invitationCode) + val result = iam.checkInvitationCode(uiState.value.invitationCode) if (result.isSuccess) { // TODO navigate to login or register screen } else {