Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heartbeat Study survey #182

Draft
wants to merge 38 commits into
base: task/heartbeat-app
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
178db06
add fakes of account manager and choir repository
eldcn Feb 15, 2025
73a2f08
add initial state handling, send verification email and handle survey…
eldcn Feb 15, 2025
64445e8
add faking of account manager
eldcn Feb 15, 2025
89f9254
save work
eldcn Feb 15, 2025
7069a99
add fake data
eldcn Feb 16, 2025
b44b86f
handover onboarding from main view model
eldcn Feb 16, 2025
98fe8ab
further adaptions
eldcn Feb 16, 2025
f2e5c33
further adaptions
eldcn Feb 16, 2025
3b85179
minor adaptions
eldcn Feb 16, 2025
61444a2
introduce fake configs and added temporary html rendering support
eldcn Feb 16, 2025
ac64970
add dropdown field
eldcn Feb 16, 2025
a73b893
add text field item
eldcn Feb 16, 2025
6f97721
add check box field item
eldcn Feb 16, 2025
d118d23
add radios field item
eldcn Feb 16, 2025
ea080e6
introduce choices field item
eldcn Feb 16, 2025
c0f6e65
add headline field item
eldcn Feb 16, 2025
0286620
use content modifier for all cards
eldcn Feb 16, 2025
b613b52
add text area
eldcn Feb 16, 2025
04d2145
add number text field
eldcn Feb 16, 2025
abc6158
add date picker form field
eldcn Feb 16, 2025
bfd5c68
add date picker form field
eldcn Feb 16, 2025
0d59cd1
remove headline field item
eldcn Feb 16, 2025
3afb23d
mark survey ui state as survey item
eldcn Feb 16, 2025
153f5ad
remove old files
eldcn Feb 16, 2025
55e7f45
map items and complete the survey
eldcn Feb 16, 2025
7cff963
add submission also on back press
eldcn Feb 16, 2025
1a6d2f6
minor adaptions
eldcn Feb 16, 2025
8db3bf1
adaptions to handle update method
eldcn Feb 17, 2025
702a275
adapt logic of text validations
eldcn Feb 17, 2025
c9d0d7d
adapt selectable dates predicate in date picker
eldcn Feb 17, 2025
4f6dadc
update nesting of assessment step and fake data
eldcn Feb 17, 2025
55397d7
minor adaptions
eldcn Feb 17, 2025
896b5fd
save non nested changes
eldcn Feb 17, 2025
8a7d6c7
add fake configs docs
eldcn Feb 17, 2025
1eaab48
enable login in fake flow
eldcn Feb 17, 2025
ce8ad06
Update app icon and LoginPage UI
pauljohanneskraft Feb 18, 2025
6160629
update
pauljohanneskraft Feb 18, 2025
ace6d56
revert ids and labels in fake data json
eldcn Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import edu.stanford.bdh.engagehf.R
import edu.stanford.spezi.core.design.component.DatePickerDialog
import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.design.theme.TextStyles
import java.time.Instant
Expand Down Expand Up @@ -53,7 +54,7 @@
}

if (showDatePicker) {
edu.stanford.spezi.module.account.register.DatePickerDialog(
DatePickerDialog(

Check warning on line 57 in app/src/main/kotlin/edu/stanford/bdh/engagehf/health/components/TimePicker.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/edu/stanford/bdh/engagehf/health/components/TimePicker.kt#L57

Added line #L57 was not covered by tests
onDateSelected = { date ->
updateDate(date)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package edu.stanford.spezi.module.account.register
package edu.stanford.spezi.core.design.component

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
Expand All @@ -11,18 +11,19 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import edu.stanford.spezi.module.account.R
import edu.stanford.spezi.core.design.R
import java.time.Instant

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DatePickerDialog(
onDateSelected: (Instant) -> Unit,
selectableDatesPredicate: (Instant) -> Boolean = { it <= Instant.now() },
onDismiss: () -> Unit,
) {
val datePickerState = rememberDatePickerState(selectableDates = object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
return utcTimeMillis <= System.currentTimeMillis()
return selectableDatesPredicate(Instant.ofEpochMilli(utcTimeMillis))

Check warning on line 26 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/DatePickerDialog.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/DatePickerDialog.kt#L26

Added line #L26 was not covered by tests
}
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.stanford.spezi.core.design.component

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusEvent
import kotlinx.coroutines.launch

@OptIn(ExperimentalFoundationApi::class)
fun Modifier.bringIntoViewOnFocusedEvent() = this then Modifier.composed {
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
onFocusEvent { focusState ->

Check warning on line 16 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt#L13-L16

Added lines #L13 - L16 were not covered by tests
if (focusState.isFocused) {
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}

Check warning on line 20 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt#L18-L20

Added lines #L18 - L20 were not covered by tests
}
}
}

Check warning on line 23 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/component/FocusEvent.kt#L22-L23

Added lines #L22 - L23 were not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@
@Composable
@ReadOnlyComposable
get() = Color.Transparent

val white
@Composable
@ReadOnlyComposable
get() = White

Check warning on line 78 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L78

Added line #L78 was not covered by tests

val black
@Composable
@ReadOnlyComposable
get() = Black

Check warning on line 83 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L83

Added line #L83 was not covered by tests

val black20
@Composable
@ReadOnlyComposable
get() = Black20

Check warning on line 88 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L88

Added line #L88 was not covered by tests

val black40
@Composable
@ReadOnlyComposable
get() = Black40

Check warning on line 93 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L93

Added line #L93 was not covered by tests

val black80
@Composable
@ReadOnlyComposable
get() = Black80

Check warning on line 98 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L98

Added line #L98 was not covered by tests

val cardinalRedLight
@Composable
@ReadOnlyComposable
get() = CardinalRedLight

Check warning on line 103 in core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt

View check run for this annotation

Codecov / codecov/patch

core/design/src/main/kotlin/edu/stanford/spezi/core/design/theme/Colors.kt#L103

Added line #L103 was not covered by tests
}

@Suppress("unused")
Expand Down
5 changes: 5 additions & 0 deletions core/design/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
</resources>
Binary file added heartbeat-app/src/main/ic_launcher-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ package edu.stanford.bdh.heartbeat.app

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import edu.stanford.spezi.core.logging.SpeziLogger

@HiltAndroidApp
class MainApplication : Application()
class MainApplication : Application() {

override fun onCreate() {
super.onCreate()

SpeziLogger.setLoggingEnabled(enabled = BuildConfig.DEBUG)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,63 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton

data class AccountInfo(
val email: String,
val name: String?,
val isEmailVerified: Boolean,
)

class AccountManager @Inject internal constructor(
interface AccountManager {
fun observeAccountInfo(): Flow<AccountInfo?>

suspend fun reloadAccountInfo(): Result<AccountInfo?>

suspend fun getToken(): Result<String>

suspend fun deleteCurrentUser(): Result<Unit>

suspend fun signOut(): Result<Unit>

suspend fun signUpWithEmailAndPassword(
email: String,
password: String,
): Result<Unit>

suspend fun sendForgotPasswordEmail(email: String): Result<Unit>

suspend fun sendVerificationEmail(): Result<Unit>

suspend fun signIn(email: String, password: String): Result<Unit>
}

@Singleton
class AccountManagerImpl @Inject internal constructor(
private val firebaseAuth: FirebaseAuth,
@Dispatching.IO private val coroutineScope: CoroutineScope,
) {
) : AccountManager {
private val logger by speziLogger()
private var userToken: String? = null

init {
observeUserTokenChanges()
}

fun observeAccountInfo(): Flow<AccountInfo?> = callbackFlow {
private fun observeUserTokenChanges() {
firebaseAuth.addIdTokenListener { auth: FirebaseAuth ->
val currentUser = auth.currentUser
if (currentUser == null) {
userToken = null
} else {
auth.currentUser?.getIdToken(false)?.addOnSuccessListener { result ->
userToken = result.token
}
}
}
}

override fun observeAccountInfo(): Flow<AccountInfo?> = callbackFlow {
val authStateListener = FirebaseAuth.AuthStateListener { _ ->
coroutineScope.launch { send(getAccountInfo()) }
}
Expand All @@ -35,44 +78,46 @@ class AccountManager @Inject internal constructor(
}
}

fun getAccountInfo(): AccountInfo? {
return firebaseAuth.currentUser?.let { user ->
AccountInfo(
email = user.email ?: "",
name = user.displayName?.takeIf { it.isNotBlank() },
isEmailVerified = user.isEmailVerified
)
}
override suspend fun reloadAccountInfo(): Result<AccountInfo?> = runCatching {
firebaseAuth.currentUser?.reload()?.await()
userToken = getToken(forceRefresh = true).getOrNull()
getAccountInfo()
}

suspend fun getToken(forceRefresh: Boolean = false): Result<String> {
override suspend fun getToken(): Result<String> {
return getToken(forceRefresh = false)
}

private suspend fun getToken(forceRefresh: Boolean): Result<String> {
val currentReceivedToken = userToken.takeIf { forceRefresh.not() }
if (currentReceivedToken != null) return Result.success(currentReceivedToken)
return runCatching {
val user = firebaseAuth.currentUser ?: error("Does not have a current user to get a token for")
val idToken = user.getIdToken(forceRefresh).await()
return@runCatching idToken.token ?: error("Id token refresh didn't include a token")
val user =
firebaseAuth.currentUser ?: error("Does not have a current user to get a token for")
val token = user.getIdToken(forceRefresh).await().token ?: error("Id token refresh didn't include a token")
userToken = token
token
}.onFailure {
logger.e { "Failed to force refresh token" }
logger.e(it) { "Failed to retrieve token" }
}
}

suspend fun deleteCurrentUser(): Result<Unit> {
override suspend fun deleteCurrentUser(): Result<Unit> {
return runCatching {
firebaseAuth.currentUser?.delete()?.await()
return@runCatching
firebaseAuth.currentUser?.delete()?.await() ?: error("User not available")
Unit
}.onFailure {
logger.e { "Failed to delete user." }
}
}

fun signOut() {
runCatching {
firebaseAuth.signOut()
}.onFailure {
logger.e { "Failed to sign out" }
}
override suspend fun signOut(): Result<Unit> = runCatching {
firebaseAuth.signOut()
}.onFailure {
logger.e { "Failed to sign out" }
}

suspend fun signUpWithEmailAndPassword(
override suspend fun signUpWithEmailAndPassword(
email: String,
password: String,
): Result<Unit> {
Expand All @@ -84,15 +129,15 @@ class AccountManager @Inject internal constructor(
}
}

suspend fun sendForgotPasswordEmail(email: String): Result<Unit> {
override suspend fun sendForgotPasswordEmail(email: String): Result<Unit> {
return runCatching {
firebaseAuth.sendPasswordResetEmail(email).await().let { }
}.onFailure { e ->
logger.e { "Error sending forgot password email: ${e.message}" }
}
}

suspend fun sendVerificationEmail(): Result<Unit> {
override suspend fun sendVerificationEmail(): Result<Unit> {
return runCatching {
firebaseAuth.currentUser?.sendEmailVerification()?.await()
return@runCatching
Expand All @@ -101,12 +146,22 @@ class AccountManager @Inject internal constructor(
}
}

suspend fun signIn(email: String, password: String): Result<Unit> {
override suspend fun signIn(email: String, password: String): Result<Unit> {
return runCatching {
val result = firebaseAuth.signInWithEmailAndPassword(email, password).await()
if (result.user == null) error("Failed to sign in, returned null user")
}.onFailure { e ->
logger.e { "Error signing in with email and password: ${e.message}" }
}
}

private fun getAccountInfo(): AccountInfo? {
return firebaseAuth.currentUser?.let { user ->
AccountInfo(
email = user.email ?: "",
name = user.displayName?.takeIf { it.isNotBlank() },
isEmailVerified = user.isEmailVerified
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package edu.stanford.bdh.heartbeat.app.account

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
Expand All @@ -13,8 +15,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import edu.stanford.spezi.core.design.theme.Spacings
import edu.stanford.spezi.core.design.theme.TextStyles

@Composable
fun LoginPage() {
Expand All @@ -40,7 +43,22 @@ private fun LoginPage(
}
}

Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Column(modifier = Modifier.fillMaxWidth().padding(Spacings.medium)) {
Text(
"Your Account",
style = TextStyles.headlineLarge,
modifier = Modifier.align(Alignment.CenterHorizontally)
)

Spacer(modifier = Modifier.size(Spacings.medium))

Text(
"You may login to your existing account. Or create a new one if you don't have one already.",
modifier = Modifier.align(Alignment.CenterHorizontally),
)

Spacer(modifier = Modifier.size(Spacings.medium))

OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = uiState.username,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class RegisterViewModel @Inject constructor(
isLoading = false,
)
}
accountManager.sendVerificationEmail().getOrNull()
}
.onFailure { error ->
_uiState.update {
Expand Down
Loading
Loading