Skip to content

Conversation

@AdamGrzybkowski
Copy link
Contributor

WOOMOB-1511

Description

This PR implements the booking flow as per the designs lxixW6aBBNkCd9ooBK70Z1-fi-1251_19683.

What's included:

  • Removal of the Cancelled state from the bottom sheet
  • Cancel booking confirmation dialog
  • A loading indicator when the cancel is in progress (a fixed delay is set for now)

What's not included:

  • API call
  • Cancel button visibility based on the cancelled state

Steps to reproduce

  1. Launch the app
  2. Go to the bookings tab
  3. Open any booking
  4. Tap the Cancel booking button

Testing information

Test Dark/Ligth theme.

The tests that have been performed

The above

Images/gif

Screen_recording_20251014_163517.mp4
  • I have considered if this change warrants release notes and have added them to RELEASE-NOTES.txt if necessary. Use the "[Internal]" label for non-user-facing changes.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a booking cancellation flow for the WooCommerce mobile app, removing the cancelled status from the attendance bottom sheet and adding a confirmation dialog with loading indicator.

  • Removes the cancelled attendance status from the bottom sheet options
  • Adds a confirmation dialog for booking cancellation with formatted message
  • Implements loading state with progress indicator during cancellation

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
BookingDetailsViewModelTest.kt Adds test coverage for cancel dialog functionality and message formatting
BookingMapperTest.kt Tests cancel dialog message generation with customer name fallbacks
strings.xml Adds new strings for cancel dialog UI and removes cancelled status description
ic_attendance_cancelled.xml Removes unused cancelled attendance icon
BookingDetailsViewState.kt Adds cancel dialog state and CancelState sealed interface
BookingDetailsViewModel.kt Implements cancel flow logic with dialog state management
BookingDetailsScreen.kt Integrates cancel dialog component into UI
CancelBookingDialog.kt New dialog component for booking cancellation confirmation
BookingAttendanceStatusBottomSheet.kt Removes cancelled status from attendance options
BookingAppointmentDetails.kt Adds loading indicator and disabled state for cancel button
BookingMapper.kt Adds cancel dialog message formatting logic

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +99 to +106
status.iconRes?.let { iconRes ->
Icon(
painter = painterResource(iconRes),
contentDescription = status.text(),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The nullable icon handling suggests that some attendance statuses may not have icons. Consider adding a consistent icon for all statuses or documenting why certain statuses lack visual representation.

Copilot uses AI. Check for mistakes.
BookingAttendanceStatus.NO_SHOW -> R.string.booking_attendance_status_no_show_desc
}.let { stringResource(it) }
else -> null
}.let { it?.let { id -> stringResource(id) } ?: "" }
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested let expressions with nullable handling make this code harder to read. Consider simplifying to: ?.let { stringResource(it) } ?: \"\"

Suggested change
}.let { it?.let { id -> stringResource(id) } ?: "" }
}?.let { stringResource(it) } ?: ""

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's wrong with AS but it complains when I do that 😕 It's wrong though, the code works just fine.

Screenshot 2025-10-14 at 17 07 33

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested the code, AS is wrong here so updated the code.

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Oct 14, 2025

📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
App NameWooCommerce-Wear Android
Platform⌚️ Wear OS
FlavorJalapeno
Build TypeDebug
Commit841bba3
Direct Downloadwoocommerce-wear-prototype-build-pr14752-841bba3.apk

@AdamGrzybkowski AdamGrzybkowski force-pushed the issue/WOOMOB-1511_cancel_flow branch from 26ba718 to bf3935b Compare October 14, 2025 15:09
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Oct 14, 2025

📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.

App NameWooCommerce Android
Platform📱 Mobile
FlavorJalapeno
Build TypeDebug
Commit841bba3
Direct Downloadwoocommerce-prototype-build-pr14752-841bba3.apk

@codecov-commenter
Copy link

codecov-commenter commented Oct 14, 2025

Codecov Report

❌ Patch coverage is 63.73626% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.30%. Comparing base (3a1495a) to head (841bba3).
⚠️ Report is 33 commits behind head on trunk.

Files with missing lines Patch % Lines
...ings/compose/BookingAttendanceStatusBottomSheet.kt 0.00% 15 Missing ⚠️
...d/ui/bookings/compose/BookingAppointmentDetails.kt 26.66% 11 Missing ⚠️
...oid/ui/bookings/details/BookingDetailsViewModel.kt 89.13% 1 Missing and 4 partials ⚠️
...m/woocommerce/android/ui/bookings/BookingMapper.kt 85.71% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk   #14752      +/-   ##
============================================
+ Coverage     38.27%   38.30%   +0.02%     
- Complexity    10010    10019       +9     
============================================
  Files          2118     2118              
  Lines        118488   118558      +70     
  Branches      15824    15831       +7     
============================================
+ Hits          45357    45412      +55     
- Misses        68917    68928      +11     
- Partials       4214     4218       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dangermattic
Copy link
Collaborator

1 Warning
⚠️ This PR is assigned to the milestone 23.5. This milestone is due in less than 2 days.
Please make sure to get it merged by then or assign it to a milestone with a later deadline.

Generated by 🚫 Danger

@hichamboushaba hichamboushaba self-assigned this Oct 15, 2025
Copy link
Member

@hichamboushaba hichamboushaba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @AdamGrzybkowski 👏

I left one remark about potentially using DialogState for managing the dialog state instead of the custom one we have, please check it out. But I won't block the PR because of it, so I'm pre-approving.

Comment on lines +70 to +79
enabled = model.cancelButtonEnabled,
) {
Text(text = stringResource(R.string.booking_details_cancel_booking_button))
if (model.cancelInProgressShown) {
CircularProgressIndicator(
color = LocalContentColor.current,
modifier = Modifier.size(24.dp)
)
} else {
Text(text = stringResource(R.string.booking_details_cancel_booking_button))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor remark, WDYT about updating WCOutlinedButton to accept a loading parameter like what we do with WCColoredButton here, and move this logic to the component.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do, please also make sure to update WCColoredButton's implementation to pass enabled = enabled && !loading, to make sure we disable button during loading.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't mind, I would prefer to open a separate PR with it just to isolate the change that goes beyond Booking Details screen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR is here #14768

Comment on lines 23 to 24
val onDismissCancelDialog: () -> Unit = {},
val onConfirmCancelBooking: () -> Unit = {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using the existing common DialogState here, I mean something like this:

data class BookingDetailsViewState(
    val toolbarTitle: String = "",
    val bookingUiState: BookingUiState? = null,
    val loadingState: BookingDetailsLoadingState = BookingDetailsLoadingState.Idle,
    val onCancelBooking: () -> Unit = {},
    val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> },
    val dialogState: DialogState? = null,
    val onRefresh: () -> Unit = {},
) {
    val shouldShowSkeleton: Boolean = bookingUiState == null && loadingState == BookingDetailsLoadingState.Refreshing
}

Then, on the screen, we would use something like the following:

viewState.dialogState?.Render()

And we can then remove completely CancelBookingDialog.

One additional advantage of the above is that dialogState will be generic enough so that it can be used for any flow that requires a modal dialog in the details screen.

Also, DialogState accepts UiString, so you won't have to use ResourceProvider:

    fun buildCancelDialogMessage(booking: Booking, resourceProvider: ResourceProvider): UiString {
        val customerName = booking.order.customerInfo?.fullName()
            ?: resourceProvider.getString(R.string.customer_detail_guest_customer)
        val serviceName = booking.order.productInfo?.name ?: "-"
        val date = detailsDateFormatter.format(booking.start)
        val time = timeRangeFormatter.format(booking.start)
        return UiString.UiStringRes(
            R.string.booking_cancel_dialog_message,
            listOf(
                UiString.UiStringText(customerName),
                UiString.UiStringText(serviceName),
                UiString.UiStringText(date),
                UiString.UiStringText(time)
            )
        )
    }

BTW, ResourceProvider doesn't play well with configuration changes, as it uses the app context behind the scenes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed the buttons use custom colors in the dialog, so this changes things a bit, but personally I would still vote to use DialogState, we can add a parameter to DialogButton to set the text color, or even a whole ButtonColors, or we can update the Render function to allow customizing the button colors if we want to keep the color logic in the UI.

Also, if we want to match the other dialogs in the app, the dismiss color should use the primary color and not onSurface.

WDYT?

Copy link
Contributor Author

@AdamGrzybkowski AdamGrzybkowski Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea, I was not aware of the DialogState. The migration is here d0c3f31

I kept the standard colors for now.

Comment on lines +102 to +105
booking,
bookingUiStateFlow,
loadingState,
cancelBookingDialogState,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was becoming quite big, so I extracted some of those to separate Flows (booking Ui state, and cancel booking dialog state)

Copy link
Member

@hichamboushaba hichamboushaba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @AdamGrzybkowski for the changes, nice work 💯

private val cancelStatusState = MutableStateFlow<CancelStatus>(CancelStatus.Idle)
private val showCancelBookingDialog = MutableStateFlow(false)

val cancelBookingDialogState = combine(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this and bookingUiStateFlow private.

val loadingState: BookingDetailsLoadingState = BookingDetailsLoadingState.Idle,
val onCancelBooking: () -> Unit = {},
val onAttendanceStatusSelected: (BookingAttendanceStatus) -> Unit = { _ -> },
val cancelBookingDialogState: DialogState? = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just sharing a thought here, no need to change it now if you prefer to keep it for later: I think using a generic name like dialogState here would be more future proof, as if we need any other AlertDialog in the screen, we can then keep the same state, and also we'll ensure we are not trying to show multiple dialogs at the same time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! Code updated.

@AdamGrzybkowski AdamGrzybkowski merged commit 408c4b5 into trunk Oct 16, 2025
16 checks passed
@AdamGrzybkowski AdamGrzybkowski deleted the issue/WOOMOB-1511_cancel_flow branch October 16, 2025 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants