diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt index fc6271c1076..39b9cc03db1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cashpayment/WooPosCashPaymentScreen.kt @@ -3,12 +3,8 @@ package com.woocommerce.android.ui.woopos.cashpayment import androidx.compose.foundation.layout.Column 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.text.KeyboardOptions -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -21,10 +17,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -38,6 +32,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButtonState +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosToolbar import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosTypedInputField import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent @@ -53,7 +48,7 @@ fun WooPosCashPaymentScreen(onNavigationEvent: (WooPosNavigationEvent) -> Unit) state = state, onAmountChanged = { viewModel.onUIEvent(WooPosCashPaymentUIEvent.AmountChanged(it)) }, onCompleteOrderClicked = { viewModel.onUIEvent(WooPosCashPaymentUIEvent.CompleteOrderClicked) }, - onBackClicked = { onNavigationEvent(WooPosNavigationEvent.BackFromCashPayment) }, + onBackClicked = { onNavigationEvent(WooPosNavigationEvent.GoBack) }, onOrderComplete = { onNavigationEvent(WooPosNavigationEvent.OpenHomeFromCashPaymentAfterSuccessfulPayment) }, ) } @@ -69,7 +64,10 @@ fun WooPosCashPaymentScreen( Column( modifier = Modifier.fillMaxSize() ) { - Toolbar(onBackClicked) + WooPosToolbar( + titleText = stringResource(R.string.woopos_cash_payment_title), + onBackClicked = onBackClicked, + ) when (state) { is WooPosCashPaymentState.Collecting -> { @@ -188,50 +186,6 @@ private fun Collecting( } } -@Composable -private fun Toolbar(onBackClicked: () -> Unit) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .padding(top = 40.dp.toAdaptivePadding()) - .height(40.dp) - ) { - val (backButton, title) = createRefs() - IconButton( - onClick = { onBackClicked() }, - modifier = Modifier - .constrainAs(backButton) { - start.linkTo(parent.start) - top.linkTo(parent.top) - centerVerticallyTo(parent) - } - .padding(start = 8.dp.toAdaptivePadding()) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_back_24dp), - contentDescription = stringResource(R.string.woopos_cart_back_content_description), - tint = MaterialTheme.colors.onBackground, - modifier = Modifier.size(28.dp) - ) - } - - val iconTitlePadding = 8.dp.toAdaptivePadding() - Text( - text = stringResource(R.string.woopos_cash_payment_title), - style = MaterialTheme.typography.h4, - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.Bold, - maxLines = 1, - modifier = Modifier - .constrainAs(title) { - top.linkTo(backButton.top) - start.linkTo(backButton.end, margin = iconTitlePadding) - centerVerticallyTo(parent) - } - ) - } -} - @WooPosPreview @Composable fun WooPosTotalsPaymentCashScreenPreview() { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosButtons.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosButtons.kt index ea0b1156c3f..a15dc84fafd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosButtons.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosButtons.kt @@ -17,6 +17,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview @@ -159,7 +160,7 @@ fun WooPosOutlinedButton( border = BorderStroke(2.dp, MaterialTheme.colors.onBackground), shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.background, + backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.onBackground, ), elevation = ButtonDefaults.elevation( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosInputFields.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosInputFields.kt index 446b95a7c94..f790e2e4876 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosInputFields.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosInputFields.kt @@ -1,10 +1,16 @@ package com.woocommerce.android.ui.woopos.common.composeui.component +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme @@ -12,9 +18,17 @@ import androidx.compose.material.Text import androidx.compose.material.TextFieldColors import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -92,9 +106,62 @@ fun WooPosTypedInputField( } } +@Composable +fun WooPosInputField( + value: String, + onValueChange: (String) -> Unit, + label: String = "", + textStyle: TextStyle = MaterialTheme.typography.h6, + textColor: Color = MaterialTheme.colors.onBackground, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + contentAlignment: Alignment = Alignment.CenterStart, + modifier: Modifier = Modifier, +) { + var labelWidth by remember { mutableIntStateOf(0) } + + Box( + modifier = modifier + .background(Color.Transparent), + contentAlignment = contentAlignment, + ) { + if (value.isEmpty()) { + Text( + text = label, + style = textStyle.copy(color = textColor.copy(alpha = 0.2f)), + maxLines = 1, + softWrap = false, + modifier = Modifier + .onGloballyPositioned { coordinates -> + labelWidth = coordinates.size.width + } + ) + } + + val density = LocalDensity.current + + // that's workaround to keep cursor to the left from the label + val textFieldModifier = if (value.isEmpty()) { + Modifier.width(with(density) { labelWidth.toDp() + 4.dp }) + } else { + Modifier.width(IntrinsicSize.Min) + } + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = textStyle.copy(color = textColor), + singleLine = true, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + modifier = textFieldModifier, + cursorBrush = SolidColor(MaterialTheme.colors.onBackground), + ) + } +} + @WooPosPreview @Composable -fun WooPosInputFieldPreview() { +fun WooPosTypedInputFieldPreview() { WooPosTheme { Column(modifier = Modifier.padding(16.dp)) { WooPosTypedInputField( @@ -125,3 +192,32 @@ fun WooPosInputFieldPreview() { } } } + +@WooPosPreview +@Composable +fun WooPosInputFieldPreview() { + WooPosTheme { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + WooPosInputField( + value = "longemail@gmail.com", + onValueChange = {}, + textStyle = MaterialTheme.typography.h3, + contentAlignment = Alignment.Center + ) + + Spacer(modifier = Modifier.size(8.dp)) + + WooPosInputField( + value = "", + onValueChange = {}, + textStyle = MaterialTheme.typography.h3, + label = "Label Label", + ) + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosToolbar.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosToolbar.kt new file mode 100644 index 00000000000..ebad905cff6 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosToolbar.kt @@ -0,0 +1,86 @@ +package com.woocommerce.android.ui.woopos.common.composeui.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import com.woocommerce.android.R +import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview +import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme +import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding + +@Composable +fun WooPosToolbar( + titleText: String, + onBackClicked: () -> Unit +) { + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp.toAdaptivePadding()) + .height(40.dp) + ) { + val (backButton, title) = createRefs() + IconButton( + onClick = { onBackClicked() }, + modifier = Modifier + .constrainAs(backButton) { + start.linkTo(parent.start) + top.linkTo(parent.top) + centerVerticallyTo(parent) + } + .padding(start = 8.dp.toAdaptivePadding()) + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_back_24dp), + contentDescription = stringResource(R.string.woopos_toolbar_icon_content_description), + tint = MaterialTheme.colors.onBackground, + modifier = Modifier.size(28.dp) + ) + } + + val iconTitlePadding = 8.dp.toAdaptivePadding() + Text( + text = titleText, + style = MaterialTheme.typography.h4, + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.Bold, + maxLines = 1, + modifier = Modifier + .constrainAs(title) { + top.linkTo(backButton.top) + start.linkTo(backButton.end, margin = iconTitlePadding) + centerVerticallyTo(parent) + } + ) + } +} + +@WooPosPreview +@Composable +fun WooPosToolbarPreview() { + WooPosTheme { + Column { + WooPosToolbar( + titleText = "Title", + onBackClicked = { } + ) + + Spacer(modifier = Modifier.weight(1f)) + } + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepository.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepository.kt index f34fd4449ae..9752434c1d3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepository.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepository.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.woopos.emailreceipt +import android.util.Patterns import com.woocommerce.android.model.Order import com.woocommerce.android.model.OrderMapper import com.woocommerce.android.tools.SelectedSite @@ -14,6 +15,7 @@ class WooPosEmailReceiptRepository @Inject constructor( private val orderStore: WCOrderStore, private val orderCreateEditRepository: OrderCreateEditRepository, private val orderMapper: OrderMapper, + private val provideEmailPattern: WooPosProvideEmailPattern, ) { suspend fun sendReceiptByEmail(orderId: Long, email: String): Result = withContext(Dispatchers.IO) { val order = getOrderById(orderId) @@ -28,6 +30,8 @@ class WooPosEmailReceiptRepository @Inject constructor( return@withContext triggerOrderReceiptSending(orderId) } + fun isEmailValid(email: String): Boolean = provideEmailPattern().matcher(email).matches() + private suspend fun triggerOrderReceiptSending(orderId: Long): Result { val sendOrderResult = orderStore.sendOrderReceipt(selectedSite.get(), orderId) return if (sendOrderResult.isError) { @@ -50,3 +54,7 @@ class WooPosEmailReceiptRepository @Inject constructor( orderMapper.toAppModel(it) } } + +class WooPosProvideEmailPattern @Inject constructor() { + operator fun invoke() = Patterns.EMAIL_ADDRESS +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptScreen.kt index 06030b27d6a..39449bbf3fb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptScreen.kt @@ -1,24 +1,38 @@ package com.woocommerce.android.ui.woopos.emailreceipt -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel +import com.woocommerce.android.R import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview +import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButtonState +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosInputField +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosToolbar +import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent @Composable -@Suppress("UnusedParameter") fun WooPosEmailReceiptScreen(onNavigationEvent: (WooPosNavigationEvent) -> Unit) { val viewModel = hiltViewModel() val state = viewModel.state.collectAsState().value @@ -27,6 +41,8 @@ fun WooPosEmailReceiptScreen(onNavigationEvent: (WooPosNavigationEvent) -> Unit) state = state, onEmailAddressChanged = { viewModel.onUIEvent(WooPosEmailReceiptUIEvent.EmailChanged(it)) }, onSendReceiptClicked = { viewModel.onUIEvent(WooPosEmailReceiptUIEvent.SendEmailClicked) }, + onBackClicked = { onNavigationEvent(WooPosNavigationEvent.GoBack) }, + onEmailSent = { onNavigationEvent(WooPosNavigationEvent.GoBack) } ) } @@ -35,63 +51,155 @@ private fun WooPosEmailReceiptScreen( state: WooPosEmailReceiptState, onEmailAddressChanged: (String) -> Unit, onSendReceiptClicked: () -> Unit, + onEmailSent: () -> Unit, + onBackClicked: () -> Unit, ) { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ConstraintLayout( - modifier = Modifier - .width(540.dp), - ) { - val (title, email, button) = createRefs() + Column(modifier = Modifier.fillMaxSize()) { + WooPosToolbar( + titleText = stringResource(R.string.woopos_email_receipt_title), + onBackClicked = onBackClicked, + ) + when (state) { + is WooPosEmailReceiptState.Email -> + EmailState( + state = state, + onEmailAddressChanged = onEmailAddressChanged, + onSendReceiptClicked = onSendReceiptClicked + ) - Text( - text = "Receipt", - style = MaterialTheme.typography.h2, - modifier = Modifier.constrainAs(title) { - top.linkTo(parent.top) - start.linkTo(parent.start) - } - ) + is WooPosEmailReceiptState.Sent -> onEmailSent() + } + } +} - TextField( - value = state.email, - onValueChange = onEmailAddressChanged, - label = { "email" }, - modifier = Modifier.constrainAs(email) { - top.linkTo(title.bottom, margin = 16.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - ) +@Composable +private fun EmailState( + state: WooPosEmailReceiptState.Email, + onEmailAddressChanged: (String) -> Unit, + onSendReceiptClicked: () -> Unit, +) { + ConstraintLayout(modifier = Modifier.fillMaxWidth()) { + val (email, error, button) = createRefs() + + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } - WooPosButton( - text = "Send", - onClick = onSendReceiptClicked, - modifier = Modifier.constrainAs(button) { - top.linkTo(email.bottom, margin = 16.dp) + val standardMargin = 16.dp.toAdaptivePadding() + val topMargin = 72.dp.toAdaptivePadding() + val textFieldButtonMargin = 80.dp.toAdaptivePadding() + WooPosInputField( + value = state.email, + onValueChange = onEmailAddressChanged, + label = stringResource(R.string.woopos_email_receipt_email_label), + contentAlignment = Alignment.Center, + textStyle = MaterialTheme.typography.h3, + keyboardOptions = KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Email + ), + modifier = Modifier.constrainAs(email) { + top.linkTo(parent.top, margin = topMargin) + start.linkTo(parent.start) + end.linkTo(parent.end) + }.focusRequester(focusRequester), + ) + + if (state.errorMessage != null) { + Text( + text = state.errorMessage, + color = MaterialTheme.colors.error, + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.constrainAs(error) { + top.linkTo(email.bottom, margin = 8.dp) start.linkTo(parent.start) end.linkTo(parent.end) - }, + } ) } + + WooPosButton( + text = state.button.text, + onClick = onSendReceiptClicked, + state = when (state.button.status) { + WooPosEmailReceiptState.Email.Button.Status.ENABLED -> WooPosButtonState.ENABLED + WooPosEmailReceiptState.Email.Button.Status.DISABLED -> WooPosButtonState.DISABLED + WooPosEmailReceiptState.Email.Button.Status.LOADING -> WooPosButtonState.LOADING + }, + modifier = Modifier.constrainAs(button) { + top.linkTo(email.bottom, margin = textFieldButtonMargin) + start.linkTo(parent.start, margin = standardMargin) + end.linkTo(parent.end, margin = standardMargin) + width = Dimension.fillToConstraints + }, + ) } } @WooPosPreview @Composable fun PreviewWooPosTotalsPaymentReceiptScreen() { - WooPosEmailReceiptScreen( - state = WooPosEmailReceiptState( - email = "email@google.com", - button = WooPosEmailReceiptState.Button( - text = "Send", - status = WooPosEmailReceiptState.Button.Status.ENABLED - ) - ), - onEmailAddressChanged = {}, - onSendReceiptClicked = {}, - ) + WooPosTheme { + WooPosEmailReceiptScreen( + state = WooPosEmailReceiptState.Email( + email = "email@google.com", + errorMessage = null, + button = WooPosEmailReceiptState.Email.Button( + text = "Send", + status = WooPosEmailReceiptState.Email.Button.Status.ENABLED + ) + ), + onEmailAddressChanged = {}, + onSendReceiptClicked = {}, + onBackClicked = {}, + onEmailSent = {}, + ) + } +} + +@WooPosPreview +@Composable +fun PreviewWooPosTotalsPaymentReceiptWithLabelScreen() { + WooPosTheme { + WooPosEmailReceiptScreen( + state = WooPosEmailReceiptState.Email( + email = "", + errorMessage = null, + button = WooPosEmailReceiptState.Email.Button( + text = "Send", + status = WooPosEmailReceiptState.Email.Button.Status.ENABLED + ) + ), + onEmailAddressChanged = {}, + onSendReceiptClicked = {}, + onEmailSent = {}, + onBackClicked = {}, + ) + } +} + +@WooPosPreview +@Composable +fun PreviewWooPosTotalsPaymentReceiptWithErrorScreen() { + WooPosTheme { + WooPosEmailReceiptScreen( + state = WooPosEmailReceiptState.Email( + email = "email@google.com", + errorMessage = "Invalid email", + button = WooPosEmailReceiptState.Email.Button( + text = "Send", + status = WooPosEmailReceiptState.Email.Button.Status.ENABLED + ) + ), + onEmailAddressChanged = {}, + onSendReceiptClicked = {}, + onBackClicked = {}, + onEmailSent = {}, + ) + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptState.kt index 3b4908b589b..2cbf7f7e01e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptState.kt @@ -4,20 +4,27 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class WooPosEmailReceiptState( - val email: String, - val button: Button -) : Parcelable { +sealed class WooPosEmailReceiptState : Parcelable { @Parcelize - data class Button( - val text: String, - val status: Status, - ) : Parcelable { + data class Email( + val email: String, + val errorMessage: String?, + val button: Button + ) : WooPosEmailReceiptState() { @Parcelize - enum class Status : Parcelable { - ENABLED, - DISABLED, - LOADING + data class Button( + val text: String, + val status: Status, + ) : Parcelable { + @Parcelize + enum class Status : Parcelable { + ENABLED, + DISABLED, + LOADING + } } } + + @Parcelize + object Sent : WooPosEmailReceiptState() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModel.kt index 9309bbf2e22..a7cdd78b234 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModel.kt @@ -8,24 +8,26 @@ import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.getStateFlow import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @Suppress("UnusedPrivateProperty") class WooPosEmailReceiptViewModel @Inject constructor( private val repository: WooPosEmailReceiptRepository, - resourceProvider: ResourceProvider, + private val resourceProvider: ResourceProvider, savedState: SavedStateHandle, ) : ViewModel() { private val orderId = savedState.get(EMAIL_RECEIPT_ROUTE_ORDER_ID_KEY)!! private val _state = savedState.getStateFlow( scope = viewModelScope, - initialValue = WooPosEmailReceiptState( + initialValue = WooPosEmailReceiptState.Email( email = "", - button = WooPosEmailReceiptState.Button( + errorMessage = null, + button = WooPosEmailReceiptState.Email.Button( text = resourceProvider.getString(R.string.woopos_email_receipt_send_button), - status = WooPosEmailReceiptState.Button.Status.DISABLED + status = WooPosEmailReceiptState.Email.Button.Status.DISABLED ) ), key = "woo_pos_email_receipt_state", @@ -35,8 +37,54 @@ class WooPosEmailReceiptViewModel @Inject constructor( fun onUIEvent(event: WooPosEmailReceiptUIEvent) { when (event) { - WooPosEmailReceiptUIEvent.SendEmailClicked -> TODO() - is WooPosEmailReceiptUIEvent.EmailChanged -> TODO() + WooPosEmailReceiptUIEvent.SendEmailClicked -> handleSendEmailClicked() + is WooPosEmailReceiptUIEvent.EmailChanged -> handleEmailChanged(event.email) + } + } + + private fun handleSendEmailClicked() { + viewModelScope.launch { + val currentState = _state.value as WooPosEmailReceiptState.Email + _state.value = currentState.copy( + errorMessage = null, + button = currentState.button.copy( + status = WooPosEmailReceiptState.Email.Button.Status.LOADING + ) + ) + + val result = repository.sendReceiptByEmail(orderId, currentState.email) + + _state.value = if (result.isSuccess) { + WooPosEmailReceiptState.Sent + } else { + val currentState = _state.value as? WooPosEmailReceiptState.Email ?: return@launch + currentState.copy( + errorMessage = resourceProvider.getString(R.string.woopos_email_receipt_send_error), + button = currentState.button.copy( + status = WooPosEmailReceiptState.Email.Button.Status.ENABLED + ) + ) + } + } + } + + private fun handleEmailChanged(email: String) { + val currentState = _state.value as WooPosEmailReceiptState.Email + _state.value = if (repository.isEmailValid(email)) { + currentState.copy( + email = email, + errorMessage = null, + button = currentState.button.copy( + status = WooPosEmailReceiptState.Email.Button.Status.ENABLED + ) + ) + } else { + currentState.copy( + email = email, + button = currentState.button.copy( + status = WooPosEmailReceiptState.Email.Button.Status.DISABLED + ) + ) } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/success/WooPosTotalsPaymentSuccessScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/success/WooPosTotalsPaymentSuccessScreen.kt index 8ce194016ff..f5c3f6a8fc2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/success/WooPosTotalsPaymentSuccessScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/success/WooPosTotalsPaymentSuccessScreen.kt @@ -33,6 +33,7 @@ import com.woocommerce.android.R import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosOutlinedButton import com.woocommerce.android.ui.woopos.common.composeui.toAdaptivePadding import com.woocommerce.android.ui.woopos.home.totals.WooPosTotalsViewState import kotlinx.coroutines.delay @@ -125,7 +126,7 @@ fun WooPosPaymentSuccessScreen( val marginBetweenButtons = 16.dp.toAdaptivePadding() if (state.isReceiptAvailable) { - WooPosButton( + WooPosOutlinedButton( modifier = Modifier .constrainAs(buttonReceipt) { bottom.linkTo(buttonNewOrder.top, margin = marginBetweenButtons) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt index 100d6b173da..8f14931f9f9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEvent.kt @@ -6,6 +6,6 @@ sealed class WooPosNavigationEvent { data object OpenHomeFromSplash : WooPosNavigationEvent() data class OpenCashPayment(val orderId: Long) : WooPosNavigationEvent() data class OpenEmailReceipt(val orderId: Long) : WooPosNavigationEvent() - data object BackFromCashPayment : WooPosNavigationEvent() + data object GoBack : WooPosNavigationEvent() data object OpenHomeFromCashPaymentAfterSuccessfulPayment : WooPosNavigationEvent() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt index d0f6d373819..6075d8fa258 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosNavigationEventHandler.kt @@ -17,7 +17,7 @@ fun NavHostController.handleNavigationEvent( is WooPosNavigationEvent.OpenHomeFromSplash -> navigateToHomeScreen() is WooPosNavigationEvent.OpenCashPayment -> navigateToCashPaymentScreen(event.orderId) - is WooPosNavigationEvent.BackFromCashPayment -> popBackStack() + is WooPosNavigationEvent.GoBack -> popBackStack() is WooPosNavigationEvent.OpenHomeFromCashPaymentAfterSuccessfulPayment -> navigateToHomeScreenAfterSuccessfulCashPayment() diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 3096b936354..95020e1112a 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4278,7 +4278,7 @@ Payment successful Payment successful checkmark icon New order - Receipt + Email receipt Subtotal Taxes Total @@ -4346,6 +4346,12 @@ Error trying to process payment. Try again. Send + Back + + Email receipt + Type email + Error trying to send this email. Try again + Customer Orders diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepositoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepositoryTest.kt index a64189e60e0..fd294f03e48 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepositoryTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptRepositoryTest.kt @@ -18,6 +18,7 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooError import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooPayload import org.wordpress.android.fluxc.store.WCOrderStore +import java.util.regex.Pattern class WooPosEmailReceiptRepositoryTest { private val siteModel: SiteModel = mock() @@ -27,14 +28,52 @@ class WooPosEmailReceiptRepositoryTest { private val orderStore: WCOrderStore = mock() private val orderCreateEditRepository: OrderCreateEditRepository = mock() private val orderMapper: OrderMapper = mock() + private val provideEmailPattern: WooPosProvideEmailPattern = mock { + on { invoke() }.thenReturn( + Pattern.compile( + "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + + "\\@" + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + + "(" + + "\\." + + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + + ")+" + ) + ) + } private val repository = WooPosEmailReceiptRepository( selectedSite, orderStore, orderCreateEditRepository, orderMapper, + provideEmailPattern ) + @Test + fun `given valid email, when isEmailValid, then return true`() { + // GIVEN + val validEmail = "test@example.com" + + // WHEN + val result = repository.isEmailValid(validEmail) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `given invalid email, when isEmailValid, then return false`() { + // GIVEN + val invalidEmail = "invalid-email" + + // WHEN + val result = repository.isEmailValid(invalidEmail) + + // THEN + assertThat(result).isFalse() + } + @Test fun `given valid order id and email, when sendReceiptByEmail, then return success`() = runTest { // GIVEN diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModelTest.kt new file mode 100644 index 00000000000..478a8a15141 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/emailreceipt/WooPosEmailReceiptViewModelTest.kt @@ -0,0 +1,135 @@ +package com.woocommerce.android.ui.woopos.emailreceipt + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.R +import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +class WooPosEmailReceiptViewModelTest { + @get:Rule + val coroutinesTestRule = WooPosCoroutineTestRule() + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val repository: WooPosEmailReceiptRepository = mock() + private val resourceProvider: ResourceProvider = mock() + + private lateinit var viewModel: WooPosEmailReceiptViewModel + + @Before + fun setUp() { + val savedStateHandle = SavedStateHandle( + mapOf(EMAIL_RECEIPT_ROUTE_ORDER_ID_KEY to 123L) + ) + + whenever(resourceProvider.getString(R.string.woopos_email_receipt_send_button)) + .thenReturn("Send") + whenever(resourceProvider.getString(R.string.woopos_email_receipt_send_error)) + .thenReturn("Error sending email") + + whenever(repository.isEmailValid(any())).thenReturn(false) + + viewModel = WooPosEmailReceiptViewModel( + repository = repository, + resourceProvider = resourceProvider, + savedState = savedStateHandle + ) + } + + @Test + fun `given initial state, when view model is created, then state should be Email with empty email and disabled button`() = runTest { + // GIVEN + + // WHEN + val state = viewModel.state.first() + + // THEN + assertThat(state).isInstanceOf(WooPosEmailReceiptState.Email::class.java) + val emailState = state as WooPosEmailReceiptState.Email + assertThat(emailState.email).isEmpty() + assertThat(emailState.errorMessage).isNull() + assertThat(emailState.button.status).isEqualTo(WooPosEmailReceiptState.Email.Button.Status.DISABLED) + } + + @Test + fun `given invalid email, when EmailChanged called, then button should remain disabled`() = runTest { + // GIVEN + whenever(repository.isEmailValid("invalid")).thenReturn(false) + + // WHEN + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.EmailChanged("invalid")) + val state = viewModel.state.first() + + // THEN + val emailState = state as WooPosEmailReceiptState.Email + assertThat(emailState.email).isEqualTo("invalid") + assertThat(emailState.button.status).isEqualTo(WooPosEmailReceiptState.Email.Button.Status.DISABLED) + } + + @Test + fun `given valid email, when EmailChanged called, then button should become enabled`() = runTest { + // GIVEN + whenever(repository.isEmailValid("valid@example.com")).thenReturn(true) + + // WHEN + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.EmailChanged("valid@example.com")) + val state = viewModel.state.first() + + // THEN + val emailState = state as WooPosEmailReceiptState.Email + assertThat(emailState.email).isEqualTo("valid@example.com") + assertThat(emailState.errorMessage).isNull() + assertThat(emailState.button.status).isEqualTo(WooPosEmailReceiptState.Email.Button.Status.ENABLED) + } + + @Test + fun `given valid email and successful send, when SendEmailClicked called, then state should become Sent`() = runTest { + // GIVEN + whenever(repository.isEmailValid("valid@example.com")).thenReturn(true) + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.EmailChanged("valid@example.com")) + whenever(repository.sendReceiptByEmail(orderId = 123L, "valid@example.com")) + .thenReturn(Result.success(Unit)) + + // WHEN + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.SendEmailClicked) + advanceUntilIdle() + + // THEN + val state = viewModel.state.value + assertThat(state).isInstanceOf(WooPosEmailReceiptState.Sent::class.java) + } + + @Test + fun `given valid email and failed send, when SendEmailClicked called, then show error message and re-enable button`() = runTest { + // GIVEN + whenever(repository.isEmailValid("valid@example.com")).thenReturn(true) + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.EmailChanged("valid@example.com")) + whenever(repository.sendReceiptByEmail(orderId = 123L, "valid@example.com")) + .thenReturn(Result.failure(RuntimeException("Failed"))) + + // WHEN + viewModel.onUIEvent(WooPosEmailReceiptUIEvent.SendEmailClicked) + advanceUntilIdle() + + // THEN + val state = viewModel.state.value + assertThat(state).isInstanceOf(WooPosEmailReceiptState.Email::class.java) + val emailState = state as WooPosEmailReceiptState.Email + assertThat(emailState.errorMessage).isEqualTo("Error sending email") + assertThat(emailState.button.status).isEqualTo(WooPosEmailReceiptState.Email.Button.Status.ENABLED) + } +}