Skip to content

[Payment Method Improvements] AppBar and Navigation for ChangeDueCalculatorFragment #11532

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -4,67 +4,24 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.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.platform.ComposeView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import com.woocommerce.android.R
import androidx.navigation.fragment.findNavController
import com.woocommerce.android.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class ChangeDueCalculatorFragment : DialogFragment() {
class ChangeDueCalculatorFragment : BaseFragment() {

private val viewModel: ChangeDueCalculatorViewModel by viewModels()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
ChangeDueCalculatorScreen()
}
}
}

@Composable
fun ChangeDueCalculatorScreen() {
val uiState by viewModel.uiState.collectAsState()

Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Display dynamic content based on UI state
when (uiState) {
is ChangeDueCalculatorViewModel.UiState.Loading -> {
Text(text = stringResource(R.string.loading), style = MaterialTheme.typography.h5)
}

is ChangeDueCalculatorViewModel.UiState.Success -> {
val state = uiState as ChangeDueCalculatorViewModel.UiState.Success
Text(
text = stringResource(R.string.cash_payments_take_payment_title, state.amountDue),
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(bottom = 16.dp)
)
}

is ChangeDueCalculatorViewModel.UiState.Error -> {
Text(text = stringResource(R.string.error_generic), style = MaterialTheme.typography.h5)
}
val uiState by viewModel.uiState.collectAsState()
ChangeDueCalculatorScreen(uiState = uiState, onNavigateUp = { findNavController().navigateUp() })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.woocommerce.android.ui.payments.methodselection

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.woocommerce.android.R

@Composable
fun ChangeDueCalculatorScreen(
uiState: ChangeDueCalculatorViewModel.UiState,
onNavigateUp: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = getTitleText(uiState)) },
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
},
backgroundColor = colorResource(id = R.color.color_toolbar),
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (uiState) {
is ChangeDueCalculatorViewModel.UiState.Loading -> Text(
stringResource(
R.string.loading,
),
style = MaterialTheme.typography.h6
)

is ChangeDueCalculatorViewModel.UiState.Success -> Text(
stringResource(
R.string.cash_payments_take_payment_title,
uiState.amountDue
),
style = MaterialTheme.typography.body1
)

is ChangeDueCalculatorViewModel.UiState.Error -> Text(
stringResource(
R.string.error_generic,
),
style = MaterialTheme.typography.h6
)
}
}
}
}

@Composable
private fun getTitleText(uiState: ChangeDueCalculatorViewModel.UiState): String {
return when (uiState) {
is ChangeDueCalculatorViewModel.UiState.Success -> stringResource(
R.string.cash_payments_take_payment_title,
uiState.amountDue
)

else -> stringResource(id = R.string.cash_payments_take_payment_title)
}
}

@Composable
@PreviewLightDark()
fun ChangeDueCalculatorScreenSuccessPreview() {
ChangeDueCalculatorScreen(
uiState = ChangeDueCalculatorViewModel.UiState.Success(
amountDue = "$100.00",
change = 0.00.toBigDecimal()
),
onNavigateUp = {}
)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package com.woocommerce.android.ui.payments.methodselection

import androidx.lifecycle.SavedStateHandle
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.details.OrderDetailRepository
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.ScopedViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.store.WooCommerceStore
import java.math.BigDecimal
import javax.inject.Inject

@HiltViewModel
class ChangeDueCalculatorViewModel @Inject constructor(
private val selectedSite: SelectedSite,
private val currencyFormatter: CurrencyFormatter,
savedStateHandle: SavedStateHandle,
private val wooCommerceStore: WooCommerceStore,
private val orderDetailRepository: OrderDetailRepository
) : ScopedViewModel(savedStateHandle) {

Expand All @@ -21,7 +27,7 @@ class ChangeDueCalculatorViewModel @Inject constructor(

sealed class UiState {
data object Loading : UiState()
data class Success(val amountDue: BigDecimal, val change: BigDecimal) : UiState()
data class Success(val amountDue: String, val change: BigDecimal) : UiState()
data object Error : UiState()
}

Expand All @@ -35,7 +41,12 @@ class ChangeDueCalculatorViewModel @Inject constructor(
private fun loadOrderDetails() {
launch {
val order = orderDetailRepository.getOrderById(orderId)!!
_uiState.value = UiState.Success(amountDue = order.total, 0.00.toBigDecimal())
_uiState.value = UiState.Success(amountDue = formatOrderTotal(order.total), 0.00.toBigDecimal())
}
}

private fun formatOrderTotal(total: BigDecimal): String {
val currencyCode = wooCommerceStore.getSiteSettings(selectedSite.get())?.currencyCode ?: ""
return currencyFormatter.formatCurrency(total, currencyCode)
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,98 @@
package com.woocommerce.android.ui.payments.methodselection

import androidx.lifecycle.SavedStateHandle
import com.woocommerce.android.model.Order
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.details.OrderDetailRepository
import com.woocommerce.android.util.CurrencyFormatter
import com.woocommerce.android.viewmodel.BaseUnitTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.WooCommerceStore
import java.math.BigDecimal

private const val ORDER_TOTAL = "100.00"

@ExperimentalCoroutinesApi
class ChangeDueCalculatorViewModelTest : BaseUnitTest() {

private val site: SiteModel = mock()
private val order: Order = mock {
on { total }.thenReturn(BigDecimal(1L))
}

private val selectedSite: SelectedSite = mock {
on { get() }.thenReturn(site)
}
private val currencyFormatter: CurrencyFormatter = mock {
on { formatCurrency(any<BigDecimal>(), any(), any()) }.thenReturn(ORDER_TOTAL)
}
private val wooCommerceStore: WooCommerceStore = mock {
on { getSiteSettings(site) }.thenReturn(mock())
}

private val orderDetailRepository: OrderDetailRepository = mock()

private val savedStateHandle: SavedStateHandle = mock()

private lateinit var viewModel: ChangeDueCalculatorViewModel

@Test
fun `given valid order id, when order details are requested, then success state is emitted`() = testBlocking {
fun `when ViewModel is initialized, then initial state is loading`() {
// GIVEN
// TODO val viewModel: ChangeDueCalculatorViewModel = mock()
// TODO
whenever(savedStateHandle.get<Long>("orderId")).thenReturn(1L)

// WHEN
// TODO
viewModel = initViewModel()

// THEN
// TODO
assertThat(viewModel.uiState.value).isInstanceOf(ChangeDueCalculatorViewModel.UiState.Loading::class.java)
}

@Test
fun `given order details retrieval failure, when order details are loaded, then error state is emitted`() = testBlocking {
fun `given valid order details, when order details are requested, then success state is emitted`() = runTest {
// GIVEN
// TODO val viewModel: ChangeDueCalculatorViewModel = mock()
val expectedAmountDue = "100.00"
val expectedChange = BigDecimal("0.0")
whenever(orderDetailRepository.getOrderById(1L)).thenReturn(order)
whenever(savedStateHandle.get<Long>("orderId")).thenReturn(1L)

// WHEN
viewModel = initViewModel()

// THEN
val uiState = viewModel.uiState.value
assertThat(uiState).isInstanceOf(ChangeDueCalculatorViewModel.UiState.Success::class.java)
uiState as ChangeDueCalculatorViewModel.UiState.Success
assertThat(uiState.change).isEqualTo(expectedChange)
assertThat(uiState.amountDue).isEqualTo(expectedAmountDue)
}

@Test
fun `given order details retrieval failure, when order details are loaded, then error state is emitted`() = runTest {
// GIVEN
// TODO

// WHEN
// TODO

// THEN
// TODO assertThat(viewModel.uiState.value).isEqualTo(ChangeDueCalculatorViewModel.UiState.Error)
// TODO
}

private fun initViewModel(): ChangeDueCalculatorViewModel {
return ChangeDueCalculatorViewModel(
selectedSite,
currencyFormatter,
savedStateHandle,
wooCommerceStore,
orderDetailRepository
)
}
}
Loading