From d553008bc9949ce46b534201affbe75fd298abf9 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 11:54:03 +0100 Subject: [PATCH 01/15] Allow adding refund reason --- .../woopos/orders/WooPosIssueRefundDialog.kt | 143 +++++++++++++--- .../ui/woopos/orders/WooPosRefundState.kt | 2 + .../ui/woopos/orders/WooPosRefundUIEvent.kt | 4 + .../ui/woopos/orders/WooPosRefundViewModel.kt | 15 +- WooCommerce/src/main/res/values/strings.xml | 2 + .../orders/WooPosRefundViewModelTest.kt | 152 +++++++++++++++++- 6 files changed, 295 insertions(+), 23 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 1e8c419ecb84..11cc9128968b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -1,5 +1,7 @@ package com.woocommerce.android.ui.woopos.orders +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,6 +22,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -93,16 +97,34 @@ fun WooPosIssueRefundDialog( } WooPosRefundState.Content.RefundStep.ReviewRefund -> { - ReviewRefundContent( - state = currentState, - onDismissRequest = handleDismiss, - onContinue = { - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToConfirmRefundClicked) - }, - onEditRefund = { - viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) - } - ) + if (currentState.isEditingReason) { + RefundReasonInputForm( + state = currentState, + onReasonChanged = { reason -> + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) + }, + onSave = { + viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) + }, + onCancel = { + viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) + } + ) + } else { + ReviewRefundContent( + state = currentState, + onDismissRequest = handleDismiss, + onContinue = { + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToConfirmRefundClicked) + }, + onEditRefund = { + viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) + }, + onEditReason = { + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + } + ) + } } WooPosRefundState.Content.RefundStep.ConfirmRefund -> { @@ -297,6 +319,77 @@ private fun SelectItemsContent( } } +@Composable +private fun RefundReasonInputForm( + state: WooPosRefundState.Content, + onReasonChanged: (String) -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(WooPosSpacing.XLarge.value), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + WooPosText( + text = stringResource(R.string.woopos_orders_enter_refund_reason), + style = WooPosTypography.Heading, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + IconButton( + modifier = Modifier.size(48.dp), + onClick = onCancel, + ) { + Icon( + modifier = Modifier.size(32.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_close_24dp), + contentDescription = stringResource(R.string.close), + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + + OutlinedTextField( + value = state.refundReason, + onValueChange = onReasonChanged, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = WooPosSpacing.XLarge.value), + placeholder = { + WooPosText( + text = stringResource(R.string.woopos_orders_refund_reason_placeholder), + style = WooPosTypography.BodyLarge, + color = WooPosTheme.colors.onSurfaceVariantLowest + ) + }, + textStyle = WooPosTypography.BodyLarge.style, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = WooPosTheme.colors.outlineVariant, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface + ) + ) + + WooPosButton( + text = stringResource(R.string.save), + onClick = onSave, + modifier = Modifier + .fillMaxWidth() + .padding(WooPosSpacing.XLarge.value) + ) + } +} + @Composable private fun RefundDialogHeader(onDismissRequest: () -> Unit) { Row( @@ -428,7 +521,8 @@ private fun ReviewRefundContent( state: WooPosRefundState.Content, onDismissRequest: () -> Unit, onContinue: () -> Unit, - onEditRefund: () -> Unit + onEditRefund: () -> Unit, + onEditReason: () -> Unit ) { Column(modifier = Modifier.fillMaxWidth()) { ReviewRefundHeader(onDismissRequest = onDismissRequest) @@ -488,19 +582,27 @@ private fun ReviewRefundContent( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) + val editReasonText = stringResource(R.string.woopos_orders_edit_reason) WooPosText( - text = stringResource(R.string.woopos_orders_edit_reason), + text = editReasonText, style = WooPosTypography.BodyMedium, fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clickable(onClick = onEditReason) + .semantics { + contentDescription = editReasonText + } + ) + } + if (state.refundReason.isNotBlank()) { + WooPosText( + text = state.refundReason, + style = WooPosTypography.BodyMedium, + fontWeight = FontWeight.Normal, + color = WooPosTheme.colors.onSurfaceVariantHighest ) } - WooPosText( - text = "TEST: Customer bought an extra item.", - style = WooPosTypography.BodyMedium, - fontWeight = FontWeight.Normal, - color = WooPosTheme.colors.onSurfaceVariantHighest - ) } } @@ -837,7 +939,8 @@ fun ReviewRefundContentPreview() { state = state, onDismissRequest = {}, onContinue = {}, - onEditRefund = {} + onEditRefund = {}, + onEditReason = {} ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt index 3168969b00a0..e35490002e15 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt @@ -22,6 +22,8 @@ sealed class WooPosRefundState { val formattedTaxes: String, val formattedTotal: String, val paymentMethod: String, + val refundReason: String = "", + val isEditingReason: Boolean = false, val step: RefundStep ) : WooPosRefundState() { @Immutable diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt index 3b544a76b680..51e0d88ecc65 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt @@ -3,6 +3,10 @@ package com.woocommerce.android.ui.woopos.orders sealed class WooPosRefundUIEvent { data object ContinueToReviewClicked : WooPosRefundUIEvent() data object BackToSelectItemsClicked : WooPosRefundUIEvent() + data object EditReasonClicked : WooPosRefundUIEvent() + data object SaveReasonClicked : WooPosRefundUIEvent() + data object CancelReasonEditClicked : WooPosRefundUIEvent() + data class OnRefundReasonChanged(val reason: String) : WooPosRefundUIEvent() data object ContinueToConfirmRefundClicked : WooPosRefundUIEvent() data object BackToReviewClicked : WooPosRefundUIEvent() data object OnRefundConfirmed : WooPosRefundUIEvent() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt index 896b9911c56b..4d75d5fc8def 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt @@ -136,7 +136,10 @@ class WooPosRefundViewModel @AssistedInject constructor( if (currentState is WooPosRefundState.Content && currentState.step != WooPosRefundState.Content.RefundStep.Processing ) { - _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) + _state.value = currentState.copy( + step = WooPosRefundState.Content.RefundStep.SelectItems, + isEditingReason = false + ) } } else -> { @@ -147,6 +150,14 @@ class WooPosRefundViewModel @AssistedInject constructor( _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) WooPosRefundUIEvent.BackToSelectItemsClicked -> _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) + WooPosRefundUIEvent.EditReasonClicked -> + _state.value = currentState.copy(isEditingReason = true) + WooPosRefundUIEvent.SaveReasonClicked -> + _state.value = currentState.copy(isEditingReason = false) + WooPosRefundUIEvent.CancelReasonEditClicked -> + _state.value = currentState.copy(isEditingReason = false) + is WooPosRefundUIEvent.OnRefundReasonChanged -> + _state.value = currentState.copy(refundReason = event.reason) WooPosRefundUIEvent.ContinueToConfirmRefundClicked -> _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ConfirmRefund) WooPosRefundUIEvent.BackToReviewClicked -> @@ -186,7 +197,7 @@ class WooPosRefundViewModel @AssistedInject constructor( site = selectedSite.get(), orderId = contentState.orderId, amount = contentState.total, - reason = "", + reason = contentState.refundReason, restockItems = true, autoRefund = false, items = refundItems diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 777b396a0cdc..0670c4d8c9cf 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3818,6 +3818,8 @@ Refund total Refund reason + Enter refund reason + Add a reason for this refund (optional) Edit reason Edit refund Refund %1$s diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index d4f100e04ca0..549c4e76ff99 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -422,6 +422,105 @@ class WooPosRefundViewModelTest { assertThat(updatedState.step).isEqualTo(WooPosRefundState.Content.RefundStep.ReviewRefund) } + @Test + fun `given content state at ReviewRefund step, when EditReasonClicked event, then isEditingReason is true`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + val reviewState = viewModel.state.value as WooPosRefundState.Content + assertThat(reviewState.isEditingReason).isFalse() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.isEditingReason).isTrue() + } + + @Test + fun `given editing reason, when OnRefundReasonChanged event, then refundReason is updated`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + val editingState = viewModel.state.value as WooPosRefundState.Content + assertThat(editingState.refundReason).isEmpty() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Customer bought wrong item")) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.refundReason).isEqualTo("Customer bought wrong item") + } + + @Test + fun `given editing reason, when SaveReasonClicked event, then isEditingReason is false`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + val editingState = viewModel.state.value as WooPosRefundState.Content + assertThat(editingState.isEditingReason).isTrue() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.isEditingReason).isFalse() + } + + @Test + fun `given editing reason, when CancelReasonEditClicked event, then isEditingReason is false`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + val editingState = viewModel.state.value as WooPosRefundState.Content + assertThat(editingState.isEditingReason).isTrue() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.isEditingReason).isFalse() + } + @Test fun `given content state at ReviewRefund step, when BackToSelectItemsClicked event, then step changes to SelectItems`() = runTest { @@ -636,7 +735,7 @@ class WooPosRefundViewModelTest { } @Test - fun `given valid refund request, when refund confirmed, then refund store called with correct parameters`() = + fun `given valid refund request without reason, when refund confirmed, then refund store called with empty reason`() = runTest { // GIVEN val refundableItems = listOf(testRefundableItem) @@ -684,6 +783,57 @@ class WooPosRefundViewModelTest { ) } + @Test + fun `given valid refund request with reason, when refund confirmed, then refund store called with provided reason`() = + runTest { + // GIVEN + val testReason = "Customer bought wrong item" + val refundableItems = listOf(testRefundableItem) + val groupedItems = listOf( + RefundRequestItem( + itemId = 1L, + quantity = 1, + refundTotal = BigDecimal("20.00"), + refundTax = emptyList() + ) + ) + + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + whenever(groupRefundItems.invoke(refundableItems, testOrder)).thenReturn(groupedItems) + whenever( + refundStore.createItemsRefund( + site = any(), + orderId = any(), + amount = any(), + reason = any(), + restockItems = any(), + autoRefund = any(), + items = any() + ) + ).thenReturn(WooResult(testRefundModel)) + + viewModel = createViewModel() + advanceUntilIdle() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(testReason)) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed) + advanceUntilIdle() + + // THEN + verify(refundStore).createItemsRefund( + site = testSite, + orderId = testOrderId, + amount = BigDecimal("22.00"), + reason = testReason, + restockItems = true, + autoRefund = false, + items = groupedItems + ) + } + @Test fun `given refund store returns error, when refund confirmed, then state transitions to RefundError`() = runTest { From 742520b86ceab85889280fc8c17de701059b0a88 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 18:27:00 +0100 Subject: [PATCH 02/15] Update UI * Extract reason input field composable to external file --- .../woopos/orders/WooPosIssueRefundDialog.kt | 150 +++++------------- .../woopos/orders/WooPosRefundReasonScreen.kt | 95 +++++++++++ WooCommerce/src/main/res/values/strings.xml | 3 +- .../orders/WooPosRefundViewModelTest.kt | 16 +- 4 files changed, 145 insertions(+), 119 deletions(-) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 11cc9128968b..021ec2736947 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -22,8 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -63,7 +61,7 @@ fun WooPosIssueRefundDialog( hiltViewModel(key = "refund_$orderId") { factory -> factory.create(orderId) } - val state by viewModel.state.collectAsStateWithLifecycle() + val state: WooPosRefundState by viewModel.state.collectAsStateWithLifecycle() val handleDismiss = { if (viewModel.onDismissRequest()) { @@ -72,45 +70,32 @@ fun WooPosIssueRefundDialog( } } - WooPosDialogWrapper( - isVisible = true, - dialogBackgroundContentDescription = stringResource( - R.string.woopos_orders_issue_refund_content_description - ), - onDismissRequest = handleDismiss - ) { - when (val currentState = state) { - is WooPosRefundState.Loading -> { - LoadingContent() - } - - is WooPosRefundState.Content -> { - when (currentState.step) { - WooPosRefundState.Content.RefundStep.SelectItems -> { - SelectItemsContent( - state = currentState, - onDismissRequest = handleDismiss, - onContinue = { - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - } - ) - } + Box(modifier = Modifier.fillMaxSize()) { + WooPosDialogWrapper( + isVisible = true, + dialogBackgroundContentDescription = stringResource( + R.string.woopos_orders_issue_refund_content_description + ), + onDismissRequest = handleDismiss + ) { + when (val currentState = state) { + is WooPosRefundState.Loading -> { + LoadingContent() + } - WooPosRefundState.Content.RefundStep.ReviewRefund -> { - if (currentState.isEditingReason) { - RefundReasonInputForm( + is WooPosRefundState.Content -> { + when (currentState.step) { + WooPosRefundState.Content.RefundStep.SelectItems -> { + SelectItemsContent( state = currentState, - onReasonChanged = { reason -> - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) - }, - onSave = { - viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) - }, - onCancel = { - viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) + onDismissRequest = handleDismiss, + onContinue = { + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) } ) - } else { + } + + WooPosRefundState.Content.RefundStep.ReviewRefund -> { ReviewRefundContent( state = currentState, onDismissRequest = handleDismiss, @@ -125,7 +110,6 @@ fun WooPosIssueRefundDialog( } ) } - } WooPosRefundState.Content.RefundStep.ConfirmRefund -> { ConfirmRefundContent( @@ -175,6 +159,23 @@ fun WooPosIssueRefundDialog( ) } } + } + + val currentState = state + if (currentState is WooPosRefundState.Content && currentState.isEditingReason) { + WooPosRefundReasonScreen( + refundReason = currentState.refundReason, + onReasonChanged = { reason -> + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) + }, + onSave = { + viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) + }, + onCancel = { + viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) + } + ) + } } } @@ -319,77 +320,6 @@ private fun SelectItemsContent( } } -@Composable -private fun RefundReasonInputForm( - state: WooPosRefundState.Content, - onReasonChanged: (String) -> Unit, - onSave: () -> Unit, - onCancel: () -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(WooPosSpacing.XLarge.value), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - WooPosText( - text = stringResource(R.string.woopos_orders_enter_refund_reason), - style = WooPosTypography.Heading, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - IconButton( - modifier = Modifier.size(48.dp), - onClick = onCancel, - ) { - Icon( - modifier = Modifier.size(32.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_close_24dp), - contentDescription = stringResource(R.string.close), - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - - OutlinedTextField( - value = state.refundReason, - onValueChange = onReasonChanged, - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = WooPosSpacing.XLarge.value), - placeholder = { - WooPosText( - text = stringResource(R.string.woopos_orders_refund_reason_placeholder), - style = WooPosTypography.BodyLarge, - color = WooPosTheme.colors.onSurfaceVariantLowest - ) - }, - textStyle = WooPosTypography.BodyLarge.style, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = WooPosTheme.colors.outlineVariant, - focusedTextColor = MaterialTheme.colorScheme.onSurface, - unfocusedTextColor = MaterialTheme.colorScheme.onSurface - ) - ) - - WooPosButton( - text = stringResource(R.string.save), - onClick = onSave, - modifier = Modifier - .fillMaxWidth() - .padding(WooPosSpacing.XLarge.value) - ) - } -} - @Composable private fun RefundDialogHeader(onDismissRequest: () -> Unit) { Row( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt new file mode 100644 index 000000000000..d670b6f93769 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt @@ -0,0 +1,95 @@ +package com.woocommerce.android.ui.woopos.orders + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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 com.woocommerce.android.R +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton +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.designsystem.WooPosSpacing +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography + +@Composable +fun WooPosRefundReasonScreen( + refundReason: String, + onReasonChanged: (String) -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + keyboardController?.show() + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + WooPosToolbar( + titleText = stringResource(R.string.woopos_orders_refund_reason), + onBackClicked = onCancel, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .imePadding(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + WooPosInputField( + value = refundReason, + onValueChange = onReasonChanged, + label = stringResource(R.string.woopos_orders_refund_reason_placeholder), + contentAlignment = Alignment.Center, + textStyle = WooPosTypography.Heading, + textColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .focusRequester(focusRequester) + .padding(horizontal = WooPosSpacing.Medium.value) + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + WooPosButton( + text = stringResource(R.string.woopos_orders_refund_reason_add), + onClick = onSave, + modifier = Modifier + .fillMaxWidth() + .padding(WooPosSpacing.Medium.value) + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Small.value)) + } + } +} diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 0670c4d8c9cf..05a1c61bfffe 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3819,8 +3819,9 @@ Refund total Refund reason Enter refund reason - Add a reason for this refund (optional) + Reason for refunding order Edit reason + Add Edit refund Refund %1$s Are you sure you wish to process the refund %1$s via %2$s?\n\nThis action cannot be undone. diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index ad9983e5d752..b905a694a330 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -805,7 +805,7 @@ class WooPosRefundViewModelTest { whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - whenever(groupRefundItems.invoke(refundableItems, testOrder)).thenReturn(groupedItems) + whenever(groupRefundItems.invoke(eq(refundableItems), eq(testOrder), any())).thenReturn(groupedItems) whenever( refundStore.createItemsRefund( site = any(), @@ -828,13 +828,13 @@ class WooPosRefundViewModelTest { // THEN verify(refundStore).createItemsRefund( - site = testSite, - orderId = testOrderId, - amount = BigDecimal("22.00"), - reason = testReason, - restockItems = true, - autoRefund = false, - items = groupedItems + site = eq(testSite), + orderId = eq(testOrderId), + amount = argThat { this.compareTo(BigDecimal("22.00")) == 0 }, + reason = eq(testReason), + restockItems = eq(true), + autoRefund = eq(false), + items = eq(groupedItems) ) } From 15f3396adec6b3517dbe8ded787430a2973935d6 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 18:41:53 +0100 Subject: [PATCH 03/15] Refactor refund dialog and view model for improved event handling --- .../woopos/orders/WooPosIssueRefundDialog.kt | 87 +++++++++---------- .../ui/woopos/orders/WooPosRefundViewModel.kt | 70 ++++++++------- 2 files changed, 80 insertions(+), 77 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 021ec2736947..75c85ecced1c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -1,6 +1,5 @@ package com.woocommerce.android.ui.woopos.orders -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -111,55 +110,55 @@ fun WooPosIssueRefundDialog( ) } - WooPosRefundState.Content.RefundStep.ConfirmRefund -> { - ConfirmRefundContent( - state = currentState, - isProcessing = false, - onDismissRequest = handleDismiss, - onConfirm = { - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed) - }, - onBack = { - viewModel.onUIEvent(WooPosRefundUIEvent.BackToReviewClicked) - } - ) - } - WooPosRefundState.Content.RefundStep.Processing -> { - ConfirmRefundContent( - state = currentState, - isProcessing = true, - onDismissRequest = {}, - onConfirm = {}, - onBack = {} - ) + WooPosRefundState.Content.RefundStep.ConfirmRefund -> { + ConfirmRefundContent( + state = currentState, + isProcessing = false, + onDismissRequest = handleDismiss, + onConfirm = { + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed) + }, + onBack = { + viewModel.onUIEvent(WooPosRefundUIEvent.BackToReviewClicked) + } + ) + } + WooPosRefundState.Content.RefundStep.Processing -> { + ConfirmRefundContent( + state = currentState, + isProcessing = true, + onDismissRequest = {}, + onConfirm = {}, + onBack = {} + ) + } } } - } - is WooPosRefundState.Error -> { - ErrorContent( - message = currentState.message, - onDismissRequest = handleDismiss - ) - } + is WooPosRefundState.Error -> { + ErrorContent( + message = currentState.message, + onDismissRequest = handleDismiss + ) + } - is WooPosRefundState.NoRefundableItems -> { - NoItemsContent(onDismissRequest = handleDismiss) - } - is WooPosRefundState.RefundSuccess -> { - RefundSuccessContent( - state = currentState, - onDismissRequest = handleDismiss - ) - } - is WooPosRefundState.RefundError -> { - ErrorContent( - message = currentState.message, - onDismissRequest = handleDismiss - ) + is WooPosRefundState.NoRefundableItems -> { + NoItemsContent(onDismissRequest = handleDismiss) + } + is WooPosRefundState.RefundSuccess -> { + RefundSuccessContent( + state = currentState, + onDismissRequest = handleDismiss + ) + } + is WooPosRefundState.RefundError -> { + ErrorContent( + message = currentState.message, + onDismissRequest = handleDismiss + ) + } } } - } val currentState = state if (currentState is WooPosRefundState.Content && currentState.isEditingReason) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt index 519926495b97..3e86308e8aab 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt @@ -130,45 +130,49 @@ class WooPosRefundViewModel @AssistedInject constructor( fun onUIEvent(event: WooPosRefundUIEvent) { when (event) { - WooPosRefundUIEvent.DialogDismissed -> { - val currentState = _state.value - - if (currentState is WooPosRefundState.Content && - currentState.step != WooPosRefundState.Content.RefundStep.Processing - ) { - _state.value = currentState.copy( - step = WooPosRefundState.Content.RefundStep.SelectItems, - isEditingReason = false - ) - } - } + WooPosRefundUIEvent.DialogDismissed -> handleDialogDismissed() else -> { val currentState = _state.value as? WooPosRefundState.Content ?: return - - when (event) { - WooPosRefundUIEvent.ContinueToReviewClicked -> - _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) - WooPosRefundUIEvent.BackToSelectItemsClicked -> - _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) - WooPosRefundUIEvent.EditReasonClicked -> - _state.value = currentState.copy(isEditingReason = true) - WooPosRefundUIEvent.SaveReasonClicked -> - _state.value = currentState.copy(isEditingReason = false) - WooPosRefundUIEvent.CancelReasonEditClicked -> - _state.value = currentState.copy(isEditingReason = false) - is WooPosRefundUIEvent.OnRefundReasonChanged -> - _state.value = currentState.copy(refundReason = event.reason) - WooPosRefundUIEvent.ContinueToConfirmRefundClicked -> - _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ConfirmRefund) - WooPosRefundUIEvent.BackToReviewClicked -> - _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) - WooPosRefundUIEvent.OnRefundConfirmed -> processRefund(currentState) - WooPosRefundUIEvent.DialogDismissed -> Unit - } + handleContentStateEvent(event, currentState) } } } + private fun handleDialogDismissed() { + val currentState = _state.value + if (currentState is WooPosRefundState.Content && + currentState.step != WooPosRefundState.Content.RefundStep.Processing + ) { + _state.value = currentState.copy( + step = WooPosRefundState.Content.RefundStep.SelectItems, + isEditingReason = false + ) + } + } + + private fun handleContentStateEvent(event: WooPosRefundUIEvent, currentState: WooPosRefundState.Content) { + when (event) { + WooPosRefundUIEvent.ContinueToReviewClicked -> + _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) + WooPosRefundUIEvent.BackToSelectItemsClicked -> + _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) + WooPosRefundUIEvent.EditReasonClicked -> + _state.value = currentState.copy(isEditingReason = true) + WooPosRefundUIEvent.SaveReasonClicked -> + _state.value = currentState.copy(isEditingReason = false) + WooPosRefundUIEvent.CancelReasonEditClicked -> + _state.value = currentState.copy(isEditingReason = false) + is WooPosRefundUIEvent.OnRefundReasonChanged -> + _state.value = currentState.copy(refundReason = event.reason) + WooPosRefundUIEvent.ContinueToConfirmRefundClicked -> + _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ConfirmRefund) + WooPosRefundUIEvent.BackToReviewClicked -> + _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) + WooPosRefundUIEvent.OnRefundConfirmed -> processRefund(currentState) + WooPosRefundUIEvent.DialogDismissed -> Unit + } + } + private fun processRefund(contentState: WooPosRefundState.Content) { viewModelScope.launch { val currentState = _state.value From f67b691e71cf44b8cd6e871f8ce7391bd1a07b40 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 19:08:41 +0100 Subject: [PATCH 04/15] Remove unused string --- WooCommerce/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 05a1c61bfffe..715e68f77d17 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3818,7 +3818,6 @@ Refund total Refund reason - Enter refund reason Reason for refunding order Edit reason Add From 8b9d6eddc2632d4310f4c96acb384c96e32f232c Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 19:24:12 +0100 Subject: [PATCH 05/15] Handle text overflow --- .../android/ui/woopos/orders/WooPosIssueRefundDialog.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 75c85ecced1c..1dce6009e367 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -529,6 +529,8 @@ private fun ReviewRefundContent( text = state.refundReason, style = WooPosTypography.BodyMedium, fontWeight = FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, color = WooPosTheme.colors.onSurfaceVariantHighest ) } From 6bbaf0f184f910a70681b13a4039d220e032d204 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 19:28:20 +0100 Subject: [PATCH 06/15] Show "Edit reason" button text instead of "Add" if reason is not blank --- .../android/ui/woopos/orders/WooPosIssueRefundDialog.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 1dce6009e367..70587648f3f2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -511,7 +511,11 @@ private fun ReviewRefundContent( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) - val editReasonText = stringResource(R.string.woopos_orders_edit_reason) + val editReasonText = if (state.refundReason.isBlank()) { + stringResource(R.string.woopos_orders_refund_reason_add) + } else { + stringResource(R.string.woopos_orders_edit_reason) + } WooPosText( text = editReasonText, style = WooPosTypography.BodyMedium, From cf95aa0b04201788e8b63c0076270b3e2f497a9a Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 19:41:43 +0100 Subject: [PATCH 07/15] Add test --- .../orders/WooPosRefundViewModelTest.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index b905a694a330..e4dfdcc00279 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -525,6 +525,38 @@ class WooPosRefundViewModelTest { assertThat(updatedState.isEditingReason).isFalse() } + @Test + fun `given editing reason with changes, when refund edit action is canceled, then refundReason is restored to original value`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + val originalReason = "Original reason" + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(originalReason)) + viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) + + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Modified reason")) + + val editingState = viewModel.state.value as WooPosRefundState.Content + assertThat(editingState.refundReason).isEqualTo("Modified reason") + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.refundReason).isEqualTo(originalReason) + assertThat(updatedState.isEditingReason).isFalse() + } + @Test fun `given content state at ReviewRefund step, when BackToSelectItemsClicked event, then step changes to SelectItems`() = runTest { From e6492beed79e9515c82a0a1f02ba9e67fbb3a330 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 19:53:22 +0100 Subject: [PATCH 08/15] Revert original reason if reason editing is canceled --- .../android/ui/woopos/orders/WooPosRefundViewModel.kt | 10 ++++++++-- .../ui/woopos/orders/WooPosRefundViewModelTest.kt | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt index 3e86308e8aab..0b7d02200820 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt @@ -47,6 +47,7 @@ class WooPosRefundViewModel @AssistedInject constructor( val state: StateFlow = _state.asStateFlow() private var currentOrder: Order? = null + private var originalRefundReason: String = "" private val numberOfDecimalPoints: Int init { @@ -156,12 +157,17 @@ class WooPosRefundViewModel @AssistedInject constructor( _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) WooPosRefundUIEvent.BackToSelectItemsClicked -> _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) - WooPosRefundUIEvent.EditReasonClicked -> + WooPosRefundUIEvent.EditReasonClicked -> { + originalRefundReason = currentState.refundReason _state.value = currentState.copy(isEditingReason = true) + } WooPosRefundUIEvent.SaveReasonClicked -> _state.value = currentState.copy(isEditingReason = false) WooPosRefundUIEvent.CancelReasonEditClicked -> - _state.value = currentState.copy(isEditingReason = false) + _state.value = currentState.copy( + isEditingReason = false, + refundReason = originalRefundReason + ) is WooPosRefundUIEvent.OnRefundReasonChanged -> _state.value = currentState.copy(refundReason = event.reason) WooPosRefundUIEvent.ContinueToConfirmRefundClicked -> diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index e4dfdcc00279..be4b4a8cce62 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -539,6 +539,7 @@ class WooPosRefundViewModelTest { advanceUntilIdle() viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(originalReason)) viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) From a86a1489e14a7a7b0ba1685c747700ba414f6971 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 20:58:45 +0100 Subject: [PATCH 09/15] Update buttons' texts --- .../android/ui/woopos/orders/WooPosIssueRefundDialog.kt | 6 +----- .../android/ui/woopos/orders/WooPosRefundReasonScreen.kt | 8 +++++++- WooCommerce/src/main/res/values/strings.xml | 1 + 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 70587648f3f2..1dce6009e367 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -511,11 +511,7 @@ private fun ReviewRefundContent( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) - val editReasonText = if (state.refundReason.isBlank()) { - stringResource(R.string.woopos_orders_refund_reason_add) - } else { - stringResource(R.string.woopos_orders_edit_reason) - } + val editReasonText = stringResource(R.string.woopos_orders_edit_reason) WooPosText( text = editReasonText, style = WooPosTypography.BodyMedium, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt index d670b6f93769..97ac51e277cb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt @@ -82,7 +82,13 @@ fun WooPosRefundReasonScreen( Spacer(modifier = Modifier.weight(1f)) WooPosButton( - text = stringResource(R.string.woopos_orders_refund_reason_add), + text = stringResource( + if (refundReason.isBlank()) { + R.string.woopos_orders_refund_reason_add + } else { + R.string.woopos_orders_refund_reason_save + } + ), onClick = onSave, modifier = Modifier .fillMaxWidth() diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 715e68f77d17..6e20de1803f2 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3821,6 +3821,7 @@ Reason for refunding order Edit reason Add + Save Edit refund Refund %1$s Are you sure you wish to process the refund %1$s via %2$s?\n\nThis action cannot be undone. From 76315da4af81a1888b208598df2aad361ce7064b Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 21:05:49 +0100 Subject: [PATCH 10/15] Revert refund reason to original value when editing and dialog is dismissed --- .../ui/woopos/orders/WooPosRefundViewModel.kt | 7 +++- .../orders/WooPosRefundViewModelTest.kt | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt index 0b7d02200820..2fa35c791b3f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt @@ -146,7 +146,12 @@ class WooPosRefundViewModel @AssistedInject constructor( ) { _state.value = currentState.copy( step = WooPosRefundState.Content.RefundStep.SelectItems, - isEditingReason = false + isEditingReason = false, + refundReason = if (currentState.isEditingReason) { + originalRefundReason + } else { + currentState.refundReason + } ) } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index be4b4a8cce62..8e18d4633f40 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -630,6 +630,41 @@ class WooPosRefundViewModelTest { assertThat(updatedState.step).isEqualTo(WooPosRefundState.Content.RefundStep.SelectItems) } + @Test + fun `given editing reason with unsaved changes, when DialogDismissed event, then refundReason is reverted to original value`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + val originalReason = "Original reason" + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(originalReason)) + viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) + + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Unsaved changes")) + + val editingState = viewModel.state.value as WooPosRefundState.Content + assertThat(editingState.refundReason).isEqualTo("Unsaved changes") + assertThat(editingState.isEditingReason).isTrue() + + // WHEN + viewModel.onUIEvent(WooPosRefundUIEvent.DialogDismissed) + + // THEN + val updatedState = viewModel.state.value as WooPosRefundState.Content + assertThat(updatedState.refundReason).isEqualTo(originalReason) + assertThat(updatedState.isEditingReason).isFalse() + assertThat(updatedState.step).isEqualTo(WooPosRefundState.Content.RefundStep.SelectItems) + } + @Test fun `given non-content state, when onUIEvent called, then state remains unchanged`() = runTest { // GIVEN From 9acf1eb5f08763dc61ce4cf5ffcfefadec204e5d Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 8 Jan 2026 21:12:11 +0100 Subject: [PATCH 11/15] Handle system back press --- .../android/ui/woopos/orders/WooPosIssueRefundDialog.kt | 6 ++++++ .../android/ui/woopos/orders/WooPosRefundReasonScreen.kt | 3 +++ 2 files changed, 9 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index 1dce6009e367..e4b6b786cdff 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.woopos.orders +import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -77,6 +78,11 @@ fun WooPosIssueRefundDialog( ), onDismissRequest = handleDismiss ) { + val stateSnapshot = state + BackHandler(enabled = stateSnapshot !is WooPosRefundState.Content || !stateSnapshot.isEditingReason) { + handleDismiss() + } + when (val currentState = state) { is WooPosRefundState.Loading -> { LoadingContent() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt index 97ac51e277cb..70534772c31c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.woopos.orders +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,6 +36,8 @@ fun WooPosRefundReasonScreen( onSave: () -> Unit, onCancel: () -> Unit ) { + BackHandler { onCancel() } + val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current From 15f22569cd6999e0a2f023a5d4eb6429d0c0f353 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Fri, 9 Jan 2026 11:15:49 +0100 Subject: [PATCH 12/15] Clean up code --- .../woopos/orders/WooPosIssueRefundDialog.kt | 143 ++++++++---------- 1 file changed, 64 insertions(+), 79 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index e4b6b786cdff..e1d6fb76ddb0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -84,85 +84,12 @@ fun WooPosIssueRefundDialog( } when (val currentState = state) { - is WooPosRefundState.Loading -> { - LoadingContent() - } - - is WooPosRefundState.Content -> { - when (currentState.step) { - WooPosRefundState.Content.RefundStep.SelectItems -> { - SelectItemsContent( - state = currentState, - onDismissRequest = handleDismiss, - onContinue = { - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - } - ) - } - - WooPosRefundState.Content.RefundStep.ReviewRefund -> { - ReviewRefundContent( - state = currentState, - onDismissRequest = handleDismiss, - onContinue = { - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToConfirmRefundClicked) - }, - onEditRefund = { - viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) - }, - onEditReason = { - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - } - ) - } - - WooPosRefundState.Content.RefundStep.ConfirmRefund -> { - ConfirmRefundContent( - state = currentState, - isProcessing = false, - onDismissRequest = handleDismiss, - onConfirm = { - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed) - }, - onBack = { - viewModel.onUIEvent(WooPosRefundUIEvent.BackToReviewClicked) - } - ) - } - WooPosRefundState.Content.RefundStep.Processing -> { - ConfirmRefundContent( - state = currentState, - isProcessing = true, - onDismissRequest = {}, - onConfirm = {}, - onBack = {} - ) - } - } - } - - is WooPosRefundState.Error -> { - ErrorContent( - message = currentState.message, - onDismissRequest = handleDismiss - ) - } - - is WooPosRefundState.NoRefundableItems -> { - NoItemsContent(onDismissRequest = handleDismiss) - } - is WooPosRefundState.RefundSuccess -> { - RefundSuccessContent( - state = currentState, - onDismissRequest = handleDismiss - ) - } - is WooPosRefundState.RefundError -> { - ErrorContent( - message = currentState.message, - onDismissRequest = handleDismiss - ) - } + is WooPosRefundState.Loading -> LoadingContent() + is WooPosRefundState.Content -> ContentStateHandler(currentState, viewModel, handleDismiss) + is WooPosRefundState.Error -> ErrorContent(currentState.message, handleDismiss) + is WooPosRefundState.NoRefundableItems -> NoItemsContent(handleDismiss) + is WooPosRefundState.RefundSuccess -> RefundSuccessContent(currentState, handleDismiss) + is WooPosRefundState.RefundError -> ErrorContent(currentState.message, handleDismiss) } } @@ -184,6 +111,64 @@ fun WooPosIssueRefundDialog( } } +@Composable +private fun ContentStateHandler( + state: WooPosRefundState.Content, + viewModel: WooPosRefundViewModel, + onDismissRequest: () -> Unit +) { + when (state.step) { + WooPosRefundState.Content.RefundStep.SelectItems -> { + SelectItemsContent( + state = state, + onDismissRequest = onDismissRequest, + onContinue = { + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + } + ) + } + + WooPosRefundState.Content.RefundStep.ReviewRefund -> { + ReviewRefundContent( + state = state, + onDismissRequest = onDismissRequest, + onContinue = { + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToConfirmRefundClicked) + }, + onEditRefund = { + viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) + }, + onEditReason = { + viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + } + ) + } + + WooPosRefundState.Content.RefundStep.ConfirmRefund -> { + ConfirmRefundContent( + state = state, + isProcessing = false, + onDismissRequest = onDismissRequest, + onConfirm = { + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundConfirmed) + }, + onBack = { + viewModel.onUIEvent(WooPosRefundUIEvent.BackToReviewClicked) + } + ) + } + WooPosRefundState.Content.RefundStep.Processing -> { + ConfirmRefundContent( + state = state, + isProcessing = true, + onDismissRequest = {}, + onConfirm = {}, + onBack = {} + ) + } + } +} + @Composable private fun LoadingContent() { val loadingDescription = stringResource(R.string.woopos_orders_loading_refund_items) From 8830629e8b6e378ee4e2a13f3f3586cd613ca4a5 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Fri, 9 Jan 2026 14:43:26 +0100 Subject: [PATCH 13/15] Clean up code --- .../woopos/orders/WooPosIssueRefundDialog.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index e1d6fb76ddb0..f0e4c1755693 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -61,7 +61,6 @@ fun WooPosIssueRefundDialog( hiltViewModel(key = "refund_$orderId") { factory -> factory.create(orderId) } - val state: WooPosRefundState by viewModel.state.collectAsStateWithLifecycle() val handleDismiss = { if (viewModel.onDismissRequest()) { @@ -70,7 +69,12 @@ fun WooPosIssueRefundDialog( } } + BackHandler { + handleDismiss() + } + Box(modifier = Modifier.fillMaxSize()) { + val state by viewModel.state.collectAsStateWithLifecycle() WooPosDialogWrapper( isVisible = true, dialogBackgroundContentDescription = stringResource( @@ -78,11 +82,6 @@ fun WooPosIssueRefundDialog( ), onDismissRequest = handleDismiss ) { - val stateSnapshot = state - BackHandler(enabled = stateSnapshot !is WooPosRefundState.Content || !stateSnapshot.isEditingReason) { - handleDismiss() - } - when (val currentState = state) { is WooPosRefundState.Loading -> LoadingContent() is WooPosRefundState.Content -> ContentStateHandler(currentState, viewModel, handleDismiss) @@ -92,11 +91,10 @@ fun WooPosIssueRefundDialog( is WooPosRefundState.RefundError -> ErrorContent(currentState.message, handleDismiss) } } - - val currentState = state - if (currentState is WooPosRefundState.Content && currentState.isEditingReason) { + val stateSnapshot = state + if (stateSnapshot is WooPosRefundState.Content && stateSnapshot.isEditingReason) { WooPosRefundReasonScreen( - refundReason = currentState.refundReason, + refundReason = stateSnapshot.refundReason, onReasonChanged = { reason -> viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) }, From 7fcee54c968dcc3e2912523e9c7b2169a4c431a1 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Tue, 20 Jan 2026 19:06:24 +0100 Subject: [PATCH 14/15] Refactor refund reason flow to use nav host controller --- .../component/WooPosFullScreenInputLayout.kt | 101 ++++++++++++ .../woopos/orders/WooPosIssueRefundDialog.kt | 92 ++++++----- .../woopos/orders/WooPosOrdersNavigation.kt | 10 +- .../ui/woopos/orders/WooPosOrdersScreen.kt | 21 ++- .../orders/WooPosRefundReasonNavigation.kt | 61 +++++++ .../woopos/orders/WooPosRefundReasonScreen.kt | 86 ++++------ .../ui/woopos/orders/WooPosRefundState.kt | 1 - .../ui/woopos/orders/WooPosRefundUIEvent.kt | 3 - .../ui/woopos/orders/WooPosRefundViewModel.kt | 20 +-- .../root/navigation/WooPosMainFlowGraph.kt | 2 + .../root/navigation/WooPosNavigationEvent.kt | 1 + .../WooPosNavigationEventHandler.kt | 4 + .../orders/WooPosRefundViewModelTest.kt | 153 +----------------- 13 files changed, 282 insertions(+), 273 deletions(-) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosFullScreenInputLayout.kt create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosFullScreenInputLayout.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosFullScreenInputLayout.kt new file mode 100644 index 000000000000..23bc72b2bb08 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/common/composeui/component/WooPosFullScreenInputLayout.kt @@ -0,0 +1,101 @@ +package com.woocommerce.android.ui.woopos.common.composeui.component + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewFontScale +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosSpacing +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme +import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography + +@Composable +fun WooPosFullScreenInputLayout( + modifier: Modifier = Modifier, + titleText: String, + onBackClicked: () -> Unit, + centerContent: @Composable () -> Unit, + buttonText: String, + buttonState: WooPosButtonState = WooPosButtonState.ENABLED, + onButtonClicked: () -> Unit, + topContent: (@Composable () -> Unit)? = null, + bottomContent: (@Composable () -> Unit)? = null +) { + BackHandler { onBackClicked() } + + Column( + modifier = modifier.fillMaxSize() + ) { + WooPosToolbar( + titleText = titleText, + onBackClicked = onBackClicked, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .imePadding(), + verticalArrangement = Arrangement.SpaceBetween + ) { + topContent?.invoke() + + Spacer(modifier = Modifier.weight(1f)) + + centerContent() + + if (bottomContent != null) { + bottomContent() + } + + Spacer(modifier = Modifier.weight(1f)) + + WooPosButton( + text = buttonText, + onClick = onButtonClicked, + state = buttonState, + modifier = Modifier + .fillMaxWidth() + .padding(WooPosSpacing.Medium.value) + ) + + Spacer(modifier = Modifier.height(WooPosSpacing.Small.value)) + } + } +} + +@Composable +@PreviewFontScale +fun WooPosFullScreenInputLayoutPreview() { + WooPosTheme { + WooPosFullScreenInputLayout( + titleText = "Enter Information", + onBackClicked = {}, + centerContent = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + WooPosText( + text = "Sample Input", + style = WooPosTypography.Heading, + color = MaterialTheme.colorScheme.onSurface + ) + } + }, + buttonText = "Save", + onButtonClicked = {} + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt index f0e4c1755693..b99dc8d53cb6 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosIssueRefundDialog.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,18 +51,27 @@ import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosCor import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosSpacing import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography +import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent import java.math.BigDecimal @Composable fun WooPosIssueRefundDialog( orderId: Long, - onDismissRequest: () -> Unit + onDismissRequest: () -> Unit, + onNavigationEvent: (WooPosNavigationEvent) -> Unit, + refundReasonUpdate: String? = null ) { val viewModel: WooPosRefundViewModel = hiltViewModel(key = "refund_$orderId") { factory -> factory.create(orderId) } + refundReasonUpdate?.let { reason -> + LaunchedEffect(reason) { + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) + } + } + val handleDismiss = { if (viewModel.onDismissRequest()) { viewModel.onUIEvent(WooPosRefundUIEvent.DialogDismissed) @@ -73,38 +83,27 @@ fun WooPosIssueRefundDialog( handleDismiss() } - Box(modifier = Modifier.fillMaxSize()) { - val state by viewModel.state.collectAsStateWithLifecycle() - WooPosDialogWrapper( - isVisible = true, - dialogBackgroundContentDescription = stringResource( - R.string.woopos_orders_issue_refund_content_description - ), - onDismissRequest = handleDismiss - ) { - when (val currentState = state) { - is WooPosRefundState.Loading -> LoadingContent() - is WooPosRefundState.Content -> ContentStateHandler(currentState, viewModel, handleDismiss) - is WooPosRefundState.Error -> ErrorContent(currentState.message, handleDismiss) - is WooPosRefundState.NoRefundableItems -> NoItemsContent(handleDismiss) - is WooPosRefundState.RefundSuccess -> RefundSuccessContent(currentState, handleDismiss) - is WooPosRefundState.RefundError -> ErrorContent(currentState.message, handleDismiss) - } - } - val stateSnapshot = state - if (stateSnapshot is WooPosRefundState.Content && stateSnapshot.isEditingReason) { - WooPosRefundReasonScreen( - refundReason = stateSnapshot.refundReason, - onReasonChanged = { reason -> - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(reason)) - }, - onSave = { - viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) - }, - onCancel = { - viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) - } + val state by viewModel.state.collectAsStateWithLifecycle() + WooPosDialogWrapper( + isVisible = true, + dialogBackgroundContentDescription = stringResource( + R.string.woopos_orders_issue_refund_content_description + ), + onDismissRequest = handleDismiss + ) { + when (val currentState = state) { + is WooPosRefundState.Loading -> LoadingContent() + is WooPosRefundState.Content -> ContentStateHandler( + state = currentState, + orderId = orderId, + viewModel = viewModel, + onDismissRequest = handleDismiss, + onNavigationEvent = onNavigationEvent ) + is WooPosRefundState.Error -> ErrorContent(currentState.message, handleDismiss) + is WooPosRefundState.NoRefundableItems -> NoItemsContent(handleDismiss) + is WooPosRefundState.RefundSuccess -> RefundSuccessContent(currentState, handleDismiss) + is WooPosRefundState.RefundError -> ErrorContent(currentState.message, handleDismiss) } } } @@ -112,8 +111,10 @@ fun WooPosIssueRefundDialog( @Composable private fun ContentStateHandler( state: WooPosRefundState.Content, + orderId: Long, viewModel: WooPosRefundViewModel, - onDismissRequest: () -> Unit + onDismissRequest: () -> Unit, + onNavigationEvent: (WooPosNavigationEvent) -> Unit ) { when (state.step) { WooPosRefundState.Content.RefundStep.SelectItems -> { @@ -137,7 +138,12 @@ private fun ContentStateHandler( viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) }, onEditReason = { - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) + onNavigationEvent( + WooPosNavigationEvent.OpenRefundReason( + orderId = orderId, + initialReason = state.refundReason + ) + ) } ) } @@ -486,8 +492,16 @@ private fun ReviewRefundContent( Divider() + val editReasonText = stringResource(R.string.woopos_orders_edit_reason) Column( - verticalArrangement = Arrangement.spacedBy(WooPosSpacing.XSmall.value) + verticalArrangement = Arrangement.spacedBy(WooPosSpacing.XSmall.value), + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(WooPosCornerRadius.Small.value)) + .clickable(onClick = onEditReason) + .semantics { + contentDescription = editReasonText + } ) { Row( modifier = Modifier.fillMaxWidth(), @@ -500,17 +514,11 @@ private fun ReviewRefundContent( fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) - val editReasonText = stringResource(R.string.woopos_orders_edit_reason) WooPosText( text = editReasonText, style = WooPosTypography.BodyMedium, fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier - .clickable(onClick = onEditReason) - .semantics { - contentDescription = editReasonText - } + color = MaterialTheme.colorScheme.primary ) } if (state.refundReason.isNotBlank()) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersNavigation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersNavigation.kt index 8c011a5a88c7..04632721860a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersNavigation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersNavigation.kt @@ -47,11 +47,19 @@ fun NavGraphBuilder.ordersScreen( .getStateFlow(EMAIL_RECEIPT_SENT, false) .collectAsState() + val refundReasonResult = backStackEntry.savedStateHandle + .getStateFlow(REFUND_REASON_RESULT_KEY, null) + .collectAsState() + backStackEntry.savedStateHandle.remove(EMAIL_RECEIPT_SENT) + if (refundReasonResult.value != null) { + backStackEntry.savedStateHandle.remove(REFUND_REASON_RESULT_KEY) + } WooPosOrdersScreen( onNavigationEvent = onNavigationEvent, - navigatedFromEmailReceiptSent = navigatedFromEmailReceiptSent.value + navigatedFromEmailReceiptSent = navigatedFromEmailReceiptSent.value, + refundReasonResult = refundReasonResult.value ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt index 3328e419fa11..09a9f59c1f51 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosOrdersScreen.kt @@ -80,6 +80,7 @@ val WOO_POS_ORDERS_TOOLBAR_HEIGHT = 56.dp fun WooPosOrdersScreen( onNavigationEvent: (WooPosNavigationEvent) -> Unit, navigatedFromEmailReceiptSent: Boolean, + refundReasonResult: String? = null, ) { val viewModel: WooPosOrdersViewModel = hiltViewModel() val state by viewModel.state.collectAsState() @@ -109,7 +110,9 @@ fun WooPosOrdersScreen( onOrdersEmptyActionClicked = viewModel::onOrdersEmptyActionClicked, onOrdersLoadingErrorRetryButtonClicked = viewModel::onOrdersLoadingErrorRetryButtonClicked, onUIEvent = viewModel::onUIEvent, - onIssueRefundDialogDismissed = viewModel::onIssueRefundDialogDismissed + onIssueRefundDialogDismissed = viewModel::onIssueRefundDialogDismissed, + onNavigationEvent = onNavigationEvent, + refundReasonUpdate = refundReasonResult ) } @@ -129,6 +132,8 @@ private fun WooPosOrdersScreen( onOrdersLoadingErrorRetryButtonClicked: () -> Unit, onUIEvent: (WooPosOrdersUIEvent) -> Unit, onIssueRefundDialogDismissed: () -> Unit, + onNavigationEvent: (WooPosNavigationEvent) -> Unit, + refundReasonUpdate: String? = null, ) { BackHandler { onBackClicked() } @@ -177,7 +182,9 @@ private fun WooPosOrdersScreen( is WooPosOrdersState.Content.DialogState.IssueRefund -> { WooPosIssueRefundDialog( orderId = dialogState.orderId, - onDismissRequest = onIssueRefundDialogDismissed + onDismissRequest = onIssueRefundDialogDismissed, + onNavigationEvent = onNavigationEvent, + refundReasonUpdate = refundReasonUpdate ) } WooPosOrdersState.Content.DialogState.Hidden -> Unit @@ -614,7 +621,8 @@ fun WooPosOrdersScreenPreview() { onOrdersEmptyActionClicked = {}, onOrdersLoadingErrorRetryButtonClicked = {}, onUIEvent = {}, - onIssueRefundDialogDismissed = {} + onIssueRefundDialogDismissed = {}, + onNavigationEvent = {} ) } } @@ -649,7 +657,8 @@ fun WooPosOrdersSearchErrorStatePreview() { onOrdersEmptyActionClicked = {}, onOrdersLoadingErrorRetryButtonClicked = {}, onUIEvent = {}, - onIssueRefundDialogDismissed = {} + onIssueRefundDialogDismissed = {}, + onNavigationEvent = {} ) } } @@ -684,7 +693,8 @@ fun WooPosOrdersNothingFoundStatePreview() { onOrdersEmptyActionClicked = {}, onOrdersLoadingErrorRetryButtonClicked = {}, onUIEvent = {}, - onIssueRefundDialogDismissed = {} + onIssueRefundDialogDismissed = {}, + onNavigationEvent = {} ) } } @@ -710,6 +720,7 @@ fun WooPosOrdersEmptyStatePreview() { onOrdersLoadingErrorRetryButtonClicked = {}, onUIEvent = {}, onIssueRefundDialogDismissed = {}, + onNavigationEvent = {}, ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt new file mode 100644 index 000000000000..94b80a96855b --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt @@ -0,0 +1,61 @@ +package com.woocommerce.android.ui.woopos.orders + +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent +import com.woocommerce.android.ui.woopos.root.navigation.navigateOnce + +const val REFUND_REASON_RESULT_KEY = "refund_reason_result" +const val REFUND_REASON_ROUTE_ORDER_ID_KEY = "orderId" +const val REFUND_REASON_INITIAL_REASON_KEY = "initialReason" +private const val REFUND_REASON_ROUTE = + "$ORDERS_ROUTE/refund_reason/{$REFUND_REASON_ROUTE_ORDER_ID_KEY}/{$REFUND_REASON_INITIAL_REASON_KEY}" + +fun NavController.navigateToRefundReason(orderId: Long, initialReason: String = "") { + val encodedReason = java.net.URLEncoder.encode(initialReason, "UTF-8") + navigateOnce( + REFUND_REASON_ROUTE + .replace("{$REFUND_REASON_ROUTE_ORDER_ID_KEY}", orderId.toString()) + .replace("{$REFUND_REASON_INITIAL_REASON_KEY}", encodedReason) + ) +} + +fun NavGraphBuilder.refundReasonScreen( + onNavigationEvent: (WooPosNavigationEvent) -> Unit +) { + composable( + route = REFUND_REASON_ROUTE, + arguments = listOf( + navArgument(REFUND_REASON_ROUTE_ORDER_ID_KEY) { type = NavType.LongType }, + navArgument(REFUND_REASON_INITIAL_REASON_KEY) { + type = NavType.StringType + defaultValue = "" + } + ), + enterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + ) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + ) + }, + ) { backStackEntry -> + val orderId = backStackEntry.arguments?.getLong(REFUND_REASON_ROUTE_ORDER_ID_KEY) ?: 0L + val initialReason = backStackEntry.arguments?.getString(REFUND_REASON_INITIAL_REASON_KEY) + ?.let { java.net.URLDecoder.decode(it, "UTF-8") } ?: "" + + WooPosRefundReasonScreen( + orderId = orderId, + initialReason = initialReason, + onNavigationEvent = onNavigationEvent, + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt index 70534772c31c..4b5c3ff0fe1f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonScreen.kt @@ -1,21 +1,15 @@ package com.woocommerce.android.ui.woopos.orders -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement 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.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -23,20 +17,20 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import com.woocommerce.android.R -import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosButton +import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosFullScreenInputLayout 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.designsystem.WooPosSpacing import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography +import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent @Composable +@Suppress("UnusedParameter") fun WooPosRefundReasonScreen( - refundReason: String, - onReasonChanged: (String) -> Unit, - onSave: () -> Unit, - onCancel: () -> Unit + orderId: Long, + initialReason: String, + onNavigationEvent: (WooPosNavigationEvent) -> Unit ) { - BackHandler { onCancel() } + var refundReason by remember { mutableStateOf(initialReason) } val focusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current @@ -46,32 +40,19 @@ fun WooPosRefundReasonScreen( keyboardController?.show() } - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surface) - ) { - WooPosToolbar( - titleText = stringResource(R.string.woopos_orders_refund_reason), - onBackClicked = onCancel, - ) - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .imePadding(), - verticalArrangement = Arrangement.SpaceBetween - ) { - Spacer(modifier = Modifier.weight(1f)) - + WooPosFullScreenInputLayout( + titleText = stringResource(R.string.woopos_orders_refund_reason), + onBackClicked = { + onNavigationEvent(WooPosNavigationEvent.GoBack) + }, + centerContent = { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { WooPosInputField( value = refundReason, - onValueChange = onReasonChanged, + onValueChange = { refundReason = it }, label = stringResource(R.string.woopos_orders_refund_reason_placeholder), contentAlignment = Alignment.Center, textStyle = WooPosTypography.Heading, @@ -81,24 +62,21 @@ fun WooPosRefundReasonScreen( .padding(horizontal = WooPosSpacing.Medium.value) ) } - - Spacer(modifier = Modifier.weight(1f)) - - WooPosButton( - text = stringResource( - if (refundReason.isBlank()) { - R.string.woopos_orders_refund_reason_add - } else { - R.string.woopos_orders_refund_reason_save - } - ), - onClick = onSave, - modifier = Modifier - .fillMaxWidth() - .padding(WooPosSpacing.Medium.value) + }, + buttonText = stringResource( + if (refundReason.isBlank()) { + R.string.woopos_orders_refund_reason_add + } else { + R.string.woopos_orders_refund_reason_save + } + ), + onButtonClicked = { + onNavigationEvent( + WooPosNavigationEvent.GoBackWithResult( + key = REFUND_REASON_RESULT_KEY, + value = refundReason + ) ) - - Spacer(modifier = Modifier.height(WooPosSpacing.Small.value)) } - } + ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt index e35490002e15..df2ecd6306c7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundState.kt @@ -23,7 +23,6 @@ sealed class WooPosRefundState { val formattedTotal: String, val paymentMethod: String, val refundReason: String = "", - val isEditingReason: Boolean = false, val step: RefundStep ) : WooPosRefundState() { @Immutable diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt index 51e0d88ecc65..736748289a48 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundUIEvent.kt @@ -3,9 +3,6 @@ package com.woocommerce.android.ui.woopos.orders sealed class WooPosRefundUIEvent { data object ContinueToReviewClicked : WooPosRefundUIEvent() data object BackToSelectItemsClicked : WooPosRefundUIEvent() - data object EditReasonClicked : WooPosRefundUIEvent() - data object SaveReasonClicked : WooPosRefundUIEvent() - data object CancelReasonEditClicked : WooPosRefundUIEvent() data class OnRefundReasonChanged(val reason: String) : WooPosRefundUIEvent() data object ContinueToConfirmRefundClicked : WooPosRefundUIEvent() data object BackToReviewClicked : WooPosRefundUIEvent() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt index e0bfd0250999..21d3465ebbe8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModel.kt @@ -48,7 +48,6 @@ class WooPosRefundViewModel @AssistedInject constructor( val state: StateFlow = _state.asStateFlow() private var currentOrder: Order? = null - private var originalRefundReason: String = "" init { loadRefundableItems() @@ -165,13 +164,7 @@ class WooPosRefundViewModel @AssistedInject constructor( currentState.step != WooPosRefundState.Content.RefundStep.Processing ) { _state.value = currentState.copy( - step = WooPosRefundState.Content.RefundStep.SelectItems, - isEditingReason = false, - refundReason = if (currentState.isEditingReason) { - originalRefundReason - } else { - currentState.refundReason - } + step = WooPosRefundState.Content.RefundStep.SelectItems ) } } @@ -182,17 +175,6 @@ class WooPosRefundViewModel @AssistedInject constructor( _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.ReviewRefund) WooPosRefundUIEvent.BackToSelectItemsClicked -> _state.value = currentState.copy(step = WooPosRefundState.Content.RefundStep.SelectItems) - WooPosRefundUIEvent.EditReasonClicked -> { - originalRefundReason = currentState.refundReason - _state.value = currentState.copy(isEditingReason = true) - } - WooPosRefundUIEvent.SaveReasonClicked -> - _state.value = currentState.copy(isEditingReason = false) - WooPosRefundUIEvent.CancelReasonEditClicked -> - _state.value = currentState.copy( - isEditingReason = false, - refundReason = originalRefundReason - ) is WooPosRefundUIEvent.OnRefundReasonChanged -> _state.value = currentState.copy(refundReason = event.reason) WooPosRefundUIEvent.ContinueToConfirmRefundClicked -> diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosMainFlowGraph.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosMainFlowGraph.kt index fefbd5749867..fb1117ae776b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosMainFlowGraph.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/root/navigation/WooPosMainFlowGraph.kt @@ -8,6 +8,7 @@ import com.woocommerce.android.ui.woopos.home.WooPosHomeViewModel import com.woocommerce.android.ui.woopos.home.eligibilityScreen import com.woocommerce.android.ui.woopos.home.homeScreen import com.woocommerce.android.ui.woopos.orders.ordersScreen +import com.woocommerce.android.ui.woopos.orders.refundReasonScreen import com.woocommerce.android.ui.woopos.settings.settingsScreen import com.woocommerce.android.ui.woopos.splash.SPLASH_ROUTE import com.woocommerce.android.ui.woopos.splash.splashScreen @@ -29,5 +30,6 @@ fun NavGraphBuilder.mainGraph( eligibilityScreen(onNavigationEvent = onNavigationEvent) settingsScreen(onNavigationEvent = onNavigationEvent) ordersScreen(onNavigationEvent = onNavigationEvent) + refundReasonScreen(onNavigationEvent = onNavigationEvent) } } 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 a3e36dbb2da2..a1a3f38e0444 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 @@ -9,6 +9,7 @@ sealed class WooPosNavigationEvent { data object OpenHomeFromSplash : WooPosNavigationEvent() data class OpenCashPayment(val orderId: Long) : WooPosNavigationEvent() data class OpenEmailReceipt(val orderId: Long) : WooPosNavigationEvent() + data class OpenRefundReason(val orderId: Long, val initialReason: String = "") : WooPosNavigationEvent() data object GoBack : WooPosNavigationEvent() data class GoBackWithResult(val key: String, val value: Any) : 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 422ac5c3985c..2f67ed064632 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 @@ -9,6 +9,7 @@ import com.woocommerce.android.ui.woopos.home.navigateToHomeScreen import com.woocommerce.android.ui.woopos.home.navigateToHomeScreenAfterSuccessfulCashPayment import com.woocommerce.android.ui.woopos.home.navigateToHomeScreenIfHomeScreenNotOpen import com.woocommerce.android.ui.woopos.orders.navigateToOrdersScreen +import com.woocommerce.android.ui.woopos.orders.navigateToRefundReason import com.woocommerce.android.ui.woopos.settings.navigateToSettingsScreen import com.woocommerce.android.ui.woopos.splash.navigateToSplashScreen @@ -39,6 +40,9 @@ fun NavHostController.handleNavigationEvent( is WooPosNavigationEvent.OpenEmailReceipt -> navigateToEmailReceipt(event.orderId) + is WooPosNavigationEvent.OpenRefundReason -> + navigateToRefundReason(event.orderId, event.initialReason) + WooPosNavigationEvent.ReturnHomeFromCashPayment -> navigateToHomeScreenIfHomeScreenNotOpen() diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index d36fa1f7ebf5..e0a0ce83279f 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -434,45 +434,20 @@ class WooPosRefundViewModelTest { } @Test - fun `given content state at ReviewRefund step, when EditReasonClicked event, then isEditingReason is true`() = + fun `given content state, when OnRefundReasonChanged event, then refundReason is updated`() = runTest { // GIVEN val refundableItems = listOf(testRefundableItem) whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - - viewModel = createViewModel() - advanceUntilIdle() - - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - val reviewState = viewModel.state.value as WooPosRefundState.Content - assertThat(reviewState.isEditingReason).isFalse() - - // WHEN - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - - // THEN - val updatedState = viewModel.state.value as WooPosRefundState.Content - assertThat(updatedState.isEditingReason).isTrue() - } - - @Test - fun `given editing reason, when OnRefundReasonChanged event, then refundReason is updated`() = - runTest { - // GIVEN - val refundableItems = listOf(testRefundableItem) - whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) - whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + whenever(getRefundableItems.invoke(any(), any())).thenReturn(refundableItems) viewModel = createViewModel() advanceUntilIdle() viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - val editingState = viewModel.state.value as WooPosRefundState.Content - assertThat(editingState.refundReason).isEmpty() + val initialState = viewModel.state.value as WooPosRefundState.Content + assertThat(initialState.refundReason).isEmpty() // WHEN viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Customer bought wrong item")) @@ -482,89 +457,6 @@ class WooPosRefundViewModelTest { assertThat(updatedState.refundReason).isEqualTo("Customer bought wrong item") } - @Test - fun `given editing reason, when SaveReasonClicked event, then isEditingReason is false`() = - runTest { - // GIVEN - val refundableItems = listOf(testRefundableItem) - whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) - whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - - viewModel = createViewModel() - advanceUntilIdle() - - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - val editingState = viewModel.state.value as WooPosRefundState.Content - assertThat(editingState.isEditingReason).isTrue() - - // WHEN - viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) - - // THEN - val updatedState = viewModel.state.value as WooPosRefundState.Content - assertThat(updatedState.isEditingReason).isFalse() - } - - @Test - fun `given editing reason, when CancelReasonEditClicked event, then isEditingReason is false`() = - runTest { - // GIVEN - val refundableItems = listOf(testRefundableItem) - whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) - whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - - viewModel = createViewModel() - advanceUntilIdle() - - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - val editingState = viewModel.state.value as WooPosRefundState.Content - assertThat(editingState.isEditingReason).isTrue() - - // WHEN - viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) - - // THEN - val updatedState = viewModel.state.value as WooPosRefundState.Content - assertThat(updatedState.isEditingReason).isFalse() - } - - @Test - fun `given editing reason with changes, when refund edit action is canceled, then refundReason is restored to original value`() = - runTest { - // GIVEN - val refundableItems = listOf(testRefundableItem) - val originalReason = "Original reason" - whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) - whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - - viewModel = createViewModel() - advanceUntilIdle() - - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(originalReason)) - viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) - - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Modified reason")) - - val editingState = viewModel.state.value as WooPosRefundState.Content - assertThat(editingState.refundReason).isEqualTo("Modified reason") - - // WHEN - viewModel.onUIEvent(WooPosRefundUIEvent.CancelReasonEditClicked) - - // THEN - val updatedState = viewModel.state.value as WooPosRefundState.Content - assertThat(updatedState.refundReason).isEqualTo(originalReason) - assertThat(updatedState.isEditingReason).isFalse() - } - @Test fun `given content state at ReviewRefund step, when BackToSelectItemsClicked event, then step changes to SelectItems`() = runTest { @@ -637,41 +529,6 @@ class WooPosRefundViewModelTest { assertThat(updatedState.step).isEqualTo(WooPosRefundState.Content.RefundStep.SelectItems) } - @Test - fun `given editing reason with unsaved changes, when DialogDismissed event, then refundReason is reverted to original value`() = - runTest { - // GIVEN - val refundableItems = listOf(testRefundableItem) - val originalReason = "Original reason" - whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) - whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) - - viewModel = createViewModel() - advanceUntilIdle() - - viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged(originalReason)) - viewModel.onUIEvent(WooPosRefundUIEvent.SaveReasonClicked) - - viewModel.onUIEvent(WooPosRefundUIEvent.EditReasonClicked) - viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Unsaved changes")) - - val editingState = viewModel.state.value as WooPosRefundState.Content - assertThat(editingState.refundReason).isEqualTo("Unsaved changes") - assertThat(editingState.isEditingReason).isTrue() - - // WHEN - viewModel.onUIEvent(WooPosRefundUIEvent.DialogDismissed) - - // THEN - val updatedState = viewModel.state.value as WooPosRefundState.Content - assertThat(updatedState.refundReason).isEqualTo(originalReason) - assertThat(updatedState.isEditingReason).isFalse() - assertThat(updatedState.step).isEqualTo(WooPosRefundState.Content.RefundStep.SelectItems) - } - @Test fun `given non-content state, when onUIEvent called, then state remains unchanged`() = runTest { // GIVEN @@ -879,7 +736,7 @@ class WooPosRefundViewModelTest { whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) - whenever(getRefundableItems.invoke(any(), any(), any())).thenReturn(refundableItems) + whenever(getRefundableItems.invoke(any(), any())).thenReturn(refundableItems) whenever(groupRefundItems.invoke(eq(refundableItems), eq(testOrder), any())).thenReturn(groupedItems) whenever( refundStore.createItemsRefund( From be8bd8f955d7d30410f8fedf5f68e589bbe21240 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Tue, 20 Jan 2026 20:03:23 +0100 Subject: [PATCH 15/15] Clean up code --- .../orders/WooPosRefundReasonNavigation.kt | 17 +++++++-- .../orders/WooPosRefundViewModelTest.kt | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt index 94b80a96855b..d27193ce6a6b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundReasonNavigation.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.woopos.orders +import android.os.Build import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.navigation.NavController @@ -9,6 +10,7 @@ import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.woocommerce.android.ui.woopos.root.navigation.WooPosNavigationEvent import com.woocommerce.android.ui.woopos.root.navigation.navigateOnce +import java.net.URLEncoder const val REFUND_REASON_RESULT_KEY = "refund_reason_result" const val REFUND_REASON_ROUTE_ORDER_ID_KEY = "orderId" @@ -17,7 +19,11 @@ private const val REFUND_REASON_ROUTE = "$ORDERS_ROUTE/refund_reason/{$REFUND_REASON_ROUTE_ORDER_ID_KEY}/{$REFUND_REASON_INITIAL_REASON_KEY}" fun NavController.navigateToRefundReason(orderId: Long, initialReason: String = "") { - val encodedReason = java.net.URLEncoder.encode(initialReason, "UTF-8") + val encodedReason = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + URLEncoder.encode(initialReason, Charsets.UTF_8) + } else { + URLEncoder.encode(initialReason, "UTF-8") + } navigateOnce( REFUND_REASON_ROUTE .replace("{$REFUND_REASON_ROUTE_ORDER_ID_KEY}", orderId.toString()) @@ -49,8 +55,15 @@ fun NavGraphBuilder.refundReasonScreen( }, ) { backStackEntry -> val orderId = backStackEntry.arguments?.getLong(REFUND_REASON_ROUTE_ORDER_ID_KEY) ?: 0L + val initialReason = backStackEntry.arguments?.getString(REFUND_REASON_INITIAL_REASON_KEY) - ?.let { java.net.URLDecoder.decode(it, "UTF-8") } ?: "" + ?.let { encoded -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + java.net.URLDecoder.decode(encoded, Charsets.UTF_8) + } else { + java.net.URLDecoder.decode(encoded, "UTF-8") + } + } ?: "" WooPosRefundReasonScreen( orderId = orderId, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt index e0a0ce83279f..7c399bee39ad 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/orders/WooPosRefundViewModelTest.kt @@ -457,6 +457,41 @@ class WooPosRefundViewModelTest { assertThat(updatedState.refundReason).isEqualTo("Customer bought wrong item") } + @Test + fun `given content state with refund reason, when navigating between steps, then refundReason is preserved`() = + runTest { + // GIVEN + val refundableItems = listOf(testRefundableItem) + whenever(ordersDataSource.refreshOrderById(testOrderId)).thenReturn(Result.success(testOrder)) + whenever(retrieveOrderRefunds.invoke(testOrder)).thenReturn(Result.success(emptyList())) + whenever(getRefundableItems.invoke(any(), any())).thenReturn(refundableItems) + + viewModel = createViewModel() + advanceUntilIdle() + + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + viewModel.onUIEvent(WooPosRefundUIEvent.OnRefundReasonChanged("Customer changed mind")) + + val reviewState = viewModel.state.value as WooPosRefundState.Content + assertThat(reviewState.refundReason).isEqualTo("Customer changed mind") + + // WHEN - Navigate back to SelectItems + viewModel.onUIEvent(WooPosRefundUIEvent.BackToSelectItemsClicked) + + // THEN - Reason is preserved + val selectItemsState = viewModel.state.value as WooPosRefundState.Content + assertThat(selectItemsState.refundReason).isEqualTo("Customer changed mind") + assertThat(selectItemsState.step).isEqualTo(WooPosRefundState.Content.RefundStep.SelectItems) + + // WHEN - Navigate forward to ReviewRefund again + viewModel.onUIEvent(WooPosRefundUIEvent.ContinueToReviewClicked) + + // THEN - Reason is still preserved + val reviewStateAgain = viewModel.state.value as WooPosRefundState.Content + assertThat(reviewStateAgain.refundReason).isEqualTo("Customer changed mind") + assertThat(reviewStateAgain.step).isEqualTo(WooPosRefundState.Content.RefundStep.ReviewRefund) + } + @Test fun `given content state at ReviewRefund step, when BackToSelectItemsClicked event, then step changes to SelectItems`() = runTest {