mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-142: setup create account screen (#15)
This commit is contained in:
parent
17d5475d0f
commit
a9aa2ea730
10 changed files with 271 additions and 1 deletions
|
@ -63,6 +63,13 @@ abstract class BaseViewModel<S, E, A>(
|
|||
*/
|
||||
protected abstract fun handleAction(action: A): Unit
|
||||
|
||||
/**
|
||||
* Convenience method for sending an action to the [actionChannel].
|
||||
*/
|
||||
fun trySendAction(action: A) {
|
||||
actionChannel.trySend(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for sending an internal action.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package com.x8bit.bitwarden.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Component that allows the user to input text. This composable will manage the state of
|
||||
* the user's input.
|
||||
*
|
||||
* @param label label for the text field.
|
||||
* @param initialValue initial input text.
|
||||
* @param onTextChange callback that is triggered when the input of the text field changes.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenTextField(
|
||||
label: String,
|
||||
initialValue: String = "",
|
||||
onTextChange: (String) -> Unit = {},
|
||||
) {
|
||||
var input by remember { mutableStateOf(initialValue) }
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
label = { Text(text = label) },
|
||||
value = input,
|
||||
onValueChange = {
|
||||
input = it
|
||||
onTextChange.invoke(it)
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.x8bit.bitwarden.ui.feature.createaccount
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.compose.composable
|
||||
|
||||
private const val CREATE_ACCOUNT_ROUTE = "create_account"
|
||||
|
||||
/**
|
||||
* Navigate to the create account screen.
|
||||
*/
|
||||
fun NavController.navigateToCreateAccount(navOptions: NavOptions? = null) {
|
||||
this.navigate(CREATE_ACCOUNT_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the create account screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.createAccountDestinations() {
|
||||
composable(route = CREATE_ACCOUNT_ROUTE) {
|
||||
CreateAccountScreen()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.x8bit.bitwarden.ui.feature.createaccount
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.components.BitwardenTextField
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* Top level composable for the create account screen.
|
||||
*/
|
||||
@Composable
|
||||
fun CreateAccountScreen(
|
||||
viewModel: CreateAccountViewModel = viewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
viewModel.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is CreateAccountEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
verticalArrangement = spacedBy(8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = R.string.title_create_account),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
viewModel.trySendAction(CreateAccountAction.SubmitClick)
|
||||
}
|
||||
.padding(16.dp),
|
||||
text = stringResource(id = R.string.button_submit),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
BitwardenTextField(label = stringResource(id = R.string.input_label_email))
|
||||
BitwardenTextField(label = stringResource(id = R.string.input_label_master_password))
|
||||
BitwardenTextField(label = stringResource(id = R.string.input_label_re_type_master_password))
|
||||
BitwardenTextField(label = stringResource(id = R.string.input_label_master_password_hint))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.x8bit.bitwarden.ui.feature.createaccount
|
||||
|
||||
import com.x8bit.bitwarden.ui.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Models logic for the create account screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class CreateAccountViewModel @Inject constructor() :
|
||||
BaseViewModel<CreateAccountState, CreateAccountEvent, CreateAccountAction>(
|
||||
initialState = CreateAccountState,
|
||||
) {
|
||||
|
||||
override fun handleAction(action: CreateAccountAction) {
|
||||
when (action) {
|
||||
is CreateAccountAction.SubmitClick -> handleSubmitClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSubmitClick() {
|
||||
sendEvent(CreateAccountEvent.ShowToast("TODO: Handle Submit Click"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI state for the create account screen.
|
||||
*/
|
||||
data object CreateAccountState
|
||||
|
||||
/**
|
||||
* Models events for the create account screen.
|
||||
*/
|
||||
sealed class CreateAccountEvent {
|
||||
|
||||
/**
|
||||
* Placeholder event for showing a toast. Can be removed once there are real events.
|
||||
*/
|
||||
data class ShowToast(val text: String) : CreateAccountEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the create account screen.
|
||||
*/
|
||||
sealed class CreateAccountAction {
|
||||
/**
|
||||
* User clicked submit.
|
||||
*/
|
||||
data object SubmitClick : CreateAccountAction()
|
||||
}
|
|
@ -12,6 +12,7 @@ import androidx.navigation.compose.NavHost
|
|||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.ui.components.PlaceholderComposable
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountScreen
|
||||
|
||||
/**
|
||||
* Controls root level [NavHost] for the app.
|
||||
|
@ -92,7 +93,7 @@ private const val LoginRoute = "login"
|
|||
*/
|
||||
private fun NavGraphBuilder.loginDestinations() {
|
||||
composable(LoginRoute) {
|
||||
PlaceholderComposable(text = "Login")
|
||||
CreateAccountScreen()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
<resources>
|
||||
<!--Common-->
|
||||
<string name="app_name">Bitwarden</string>
|
||||
|
||||
<!--Create Account Screen-->
|
||||
<string name="title_create_account">Create Account</string>
|
||||
<string name="input_label_email">Email address</string>
|
||||
<string name="input_label_master_password">Master password</string>
|
||||
<string name="input_label_re_type_master_password">Re-type master password</string>
|
||||
<string name="input_label_master_password_hint">Master password hint (optional)</string>
|
||||
<string name="button_submit">SUBMIT</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.x8bit.bitwarden.example.ui.feature.createaccount
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.example.ui.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountAction
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountScreen
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountViewModel
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.junit.Test
|
||||
|
||||
class CreateAccountScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on submit click should send SubmitClick action`() {
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(CreateAccountAction.SubmitClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(viewModel)
|
||||
}
|
||||
composeTestRule.onNodeWithText("SUBMIT").performClick()
|
||||
verify { viewModel.trySendAction(CreateAccountAction.SubmitClick) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.x8bit.bitwarden.example.ui.feature.createaccount
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.example.ui.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountAction
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountEvent
|
||||
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountViewModel
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CreateAccountViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `on SubmitClick should emit ShowToast`() = runTest {
|
||||
val viewModel = CreateAccountViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assert(awaitItem() is CreateAccountEvent.ShowToast)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
## Contents
|
||||
|
||||
* [Style : Kotlin](#style--kotlin)
|
||||
* [Style : ViewModels](#style--view-models)
|
||||
* [Best Practices : Kotlin](#best-practices--kotlin)
|
||||
* [Best Practices : Jetpack Compose](#best-practices--jetpack-compose)
|
||||
|
||||
|
@ -445,6 +446,9 @@ val data = databaseDataSource.getData(id)
|
|||
val data = databaseDataSource.getData(id)
|
||||
```
|
||||
|
||||
## Style : ViewModels
|
||||
|
||||
- Private functions that handle actions should be prefixed with "handle" and suffixed with the name of the action. (ex: `handleSubmitClick`)
|
||||
|
||||
## Best Practices : Kotlin
|
||||
|
||||
|
|
Loading…
Reference in a new issue