diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index c4a71953e57..307bef261fc 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -27,6 +27,7 @@ final class BookingDetailsViewModel: ObservableObject { @Published private(set) var navigationTitle = "" @Published private(set) var sections: [Section] = [] + @Published var notice: Notice? var bookingAttendanceStatus: BookingAttendanceStatus { booking.attendanceStatus @@ -233,14 +234,32 @@ extension BookingDetailsViewModel { siteID: booking.siteID, bookingID: booking.bookingID, status: newStatus - ) { error in - if let error { + ) { [weak self] error in + if let error, let self { DDLogError("⛔️ Error updating booking attendance status: \(error)") - // TODO: Show an error notice to the user + displayAttendanceStatusUpdatedErrorNotice(status: newStatus) } } stores.dispatch(action) } + + private func displayAttendanceStatusUpdatedErrorNotice(status: BookingAttendanceStatus) { + let text = String.localizedStringWithFormat( + Localization.bookingAttendanceStatusUpdateFailedMessage, + booking.bookingID + ) + self.notice = Notice( + message: text, + feedbackType: .error, + actionTitle: Localization.retryActionTitle + ) { [weak self] in + guard let self else { + return + } + + updateAttendanceStatus(to: status) + } + } } private extension BookingDetailsViewModel { @@ -362,5 +381,19 @@ private extension BookingDetailsViewModel { value: "%1$@ will no longer be able to attend “%2$@” on %3$@.", comment: "Message for the booking cancellation confirmation alert. %1$@ is customer name, %2$@ is product name, %3$@ is booking date." ) + + static let bookingAttendanceStatusUpdateFailedMessage = NSLocalizedString( + "BookingDetailsView.attendanceStatus.updateFailed.message", + value: "Unable to change attendance status of Booking #%1$d", + comment: "Content of error presented when updating the attendance status of a Booking fails. " + + "It reads: Unable to change status of Booking #{Booking number}. " + + "Parameters: %1$d - Booking number" + ) + + static let retryActionTitle = NSLocalizedString( + "BookingDetailsView.retry.action", + value: "Retry", + comment: "Retry Action" + ) } } diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index dbd5ef42022..a1064ecb958 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -103,6 +103,7 @@ struct BookingDetailsView: View { Text(viewModel.cancellationAlertMessage) } .notice($notice) + .notice($viewModel.notice) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift index 5766abba1cb..e055e11b506 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingDetailsViewModelTests.swift @@ -247,6 +247,42 @@ final class BookingDetailsViewModelTests: XCTestCase { XCTAssertEqual(status, newStatus) } + func test_error_notice_displayed_when_attendance_staus_update_fails() { + // Given + let booking = Booking.fake() + let viewModel = BookingDetailsViewModel(booking: booking, stores: storesManager) + let newStatus = BookingAttendanceStatus.checkedIn + enum TestError: Error { case generic } + + // When + viewModel.updateAttendanceStatus(to: newStatus) + + // Then + XCTAssertEqual(storesManager.receivedActions.count, 1) + guard let action = storesManager.receivedActions.first as? BookingAction else { + XCTFail("Incorrect action type dispatched") + return + } + + guard case let .updateBookingAttendanceStatus(_, _, _, onCompletion) = action else { + XCTFail("Incorrect action case dispatched") + return + } + + onCompletion(TestError.generic) + + XCTAssertNotNil(viewModel.notice) + XCTAssertEqual(viewModel.notice?.feedbackType, .error) + + let messageFormat = NSLocalizedString( + "BookingDetailsView.attendanceStatus.updateFailed.message", + value: "Unable to change attendance status of Booking #%1$d", + comment: "" + ) + let expectedMessage = String(format: messageFormat, booking.bookingID) + XCTAssertEqual(viewModel.notice?.message, expectedMessage) + } + func test_init_whenBookingHasStatusAndAttendanceStatus_updatesHeaderContentWithCorrectLocalizedStrings() { // Given let booking = Booking.fake().copy(