Skip to content

Commit 89992ef

Browse files
committed
Merge branch 'trunk' into WOOMOB-1653-handle-order-details-re-selection-issue
2 parents 79bf356 + 187c7e5 commit 89992ef

File tree

12 files changed

+186
-106
lines changed

12 files changed

+186
-106
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
9999
case .ciabBookings:
100100
return buildConfig == .localDeveloper || buildConfig == .alpha
101101
case .pointOfSaleSurveys:
102-
return buildConfig == .localDeveloper || buildConfig == .alpha
102+
return true
103103
case .pointOfSaleCatalogAPI:
104104
return false
105105
default:

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [*] Improve card payments onboarding error handling to show network errors correctly [https://github.com/woocommerce/woocommerce-ios/pull/16304]
88
- [*] Authenticate the admin page automatically for sites with SSO enabled in custom fields, in-person payment setup, and editing tax rates flows. [https://github.com/woocommerce/woocommerce-ios/pull/16318]
99
- [*] Fix order details presentation when opened from booking details [https://github.com/woocommerce/woocommerce-ios/pull/16331]
10+
- [*] Show POS feedback surveys for eligible merchants [https://github.com/woocommerce/woocommerce-ios/pull/16325]
1011

1112
23.6
1213
-----

WooCommerce/Classes/Bookings/BookingList/BookingListContainerView.swift

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,12 @@ struct BookingListContainerView: View {
1616
}
1717

1818
var body: some View {
19-
VStack(spacing: 0) {
19+
BookingListView(
20+
viewModel: viewModel.listViewModel(for: viewModel.selectedTab),
21+
searchViewModel: viewModel.searchViewModel(for: viewModel.selectedTab),
22+
selectedBooking: $selectedBooking
23+
) {
2024
headerView
21-
TabView(selection: $viewModel.selectedTab) {
22-
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
23-
BookingListView(
24-
viewModel: viewModel.listViewModel(for: tab),
25-
searchViewModel: viewModel.searchViewModel(for: tab),
26-
selectedBooking: $selectedBooking
27-
)
28-
.tag(tab)
29-
}
30-
}
31-
.tabViewStyle(.page(indexDisplayMode: .never))
3225
}
3326
.navigationTitle(Localization.viewTitle)
3427
.if(isSearching, transform: { view in

WooCommerce/Classes/Bookings/BookingList/BookingListView.swift

Lines changed: 90 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SwiftUI
22
import struct Yosemite.Booking
33

4-
struct BookingListView: View {
4+
struct BookingListView<Header: View>: View {
55
@ObservedObject private var viewModel: BookingListViewModel
66
@ObservedObject private var searchViewModel: BookingSearchViewModel
77

@@ -10,12 +10,16 @@ struct BookingListView: View {
1010

1111
@Binding var selectedBooking: Booking?
1212

13+
private let header: Header
14+
1315
init(viewModel: BookingListViewModel,
1416
searchViewModel: BookingSearchViewModel,
15-
selectedBooking: Binding<Booking?>) {
17+
selectedBooking: Binding<Booking?>,
18+
@ViewBuilder header: () -> Header) {
1619
self.viewModel = viewModel
1720
self.searchViewModel = searchViewModel
1821
self._selectedBooking = selectedBooking
22+
self.header = header()
1923
}
2024

2125
var body: some View {
@@ -98,20 +102,25 @@ private extension BookingListView {
98102
onNextPage: @escaping () -> Void,
99103
onRefresh: @escaping () async -> Void) -> some View {
100104
List(selection: $selectedBooking) {
101-
ForEach(bookings) { item in
102-
bookingItem(item)
103-
.tag(item)
104-
}
105-
106-
InfiniteScrollIndicator(showContent: viewModel.shouldShowBottomActivityIndicator)
107-
.padding(.top, Layout.viewPadding)
108-
.onAppear {
109-
onNextPage()
105+
Section {
106+
ForEach(bookings) { item in
107+
bookingItem(item)
108+
.tag(item)
110109
}
110+
111+
InfiniteScrollIndicator(showContent: viewModel.shouldShowBottomActivityIndicator)
112+
.padding(.top, BookingListViewLayout.viewPadding)
113+
.onAppear {
114+
onNextPage()
115+
}
116+
} header: {
117+
header
118+
.listRowInsets(EdgeInsets())
119+
}
111120
}
112121
.listStyle(.plain)
122+
.listSectionSeparator(.hidden, edges: .top)
113123
.background(Color(.listBackground))
114-
.accentColor(Color(.listSelectedBackground))
115124
.refreshable {
116125
await onRefresh()
117126
}
@@ -140,50 +149,20 @@ private extension BookingListView {
140149
}
141150
}
142151
}
152+
.listRowBackground(booking == selectedBooking ? Color(.listSelectedBackground) : Color(.listForeground(modal: false)))
143153
}
144154

145155
func emptyStateView(isSearching: Bool, onRefresh: @escaping () async -> Void) -> some View {
146156
GeometryReader { proxy in
147157
ScrollView {
148-
VStack(spacing: Layout.emptyStatePadding) {
149-
Spacer()
150-
Image(uiImage: isSearching ? .magnifyingGlassNotFound : .noBookings)
151-
.resizable()
152-
.aspectRatio(contentMode: .fit)
153-
.frame(width: Layout.emptyStateImageWidth * scale)
154-
.padding(.bottom, Layout.viewPadding)
155-
if isSearching {
156-
Text(Localization.emptySearchText)
157-
.font(.body)
158-
.foregroundStyle(Color.secondary)
159-
} else {
160-
VStack(spacing: Layout.textVerticalPadding) {
161-
Text(viewModel.emptyStateTitle)
162-
.font(.title2)
163-
.fontWeight(.semibold)
164-
.foregroundStyle(.primary)
165-
Text(viewModel.emptyStateDescription)
166-
.font(.title3)
167-
.foregroundStyle(.secondary)
168-
}
169-
if viewModel.hasFilters {
170-
VStack(spacing: Layout.textVerticalPadding) {
171-
Button("Change filters") {
172-
// TODO
173-
}
174-
.buttonStyle(PrimaryButtonStyle())
175-
Button("Clear filters") {
176-
// TODO
177-
}
178-
.buttonStyle(SecondaryButtonStyle())
179-
}
180-
}
158+
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
159+
Section {
160+
emptyStateContent(isSearching: isSearching)
161+
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
162+
} header: {
163+
header
181164
}
182-
Spacer()
183165
}
184-
.multilineTextAlignment(.center)
185-
.padding(.horizontal, Layout.emptyStatePadding)
186-
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
187166
}
188167
.refreshable {
189168
await onRefresh()
@@ -192,41 +171,79 @@ private extension BookingListView {
192171
.background(Color(.systemBackground))
193172
}
194173

174+
func emptyStateContent(isSearching: Bool) -> some View {
175+
VStack(spacing: BookingListViewLayout.emptyStatePadding) {
176+
Spacer()
177+
Image(uiImage: isSearching ? .magnifyingGlassNotFound : .noBookings)
178+
.resizable()
179+
.aspectRatio(contentMode: .fit)
180+
.frame(width: BookingListViewLayout.emptyStateImageWidth * scale)
181+
.padding(.bottom, BookingListViewLayout.viewPadding)
182+
if isSearching {
183+
Text(BookingListViewLocalization.emptySearchText)
184+
.font(.body)
185+
.foregroundStyle(Color.secondary)
186+
} else {
187+
VStack(spacing: BookingListViewLayout.textVerticalPadding) {
188+
Text(viewModel.emptyStateTitle)
189+
.font(.title2)
190+
.fontWeight(.semibold)
191+
.foregroundStyle(.primary)
192+
Text(viewModel.emptyStateDescription)
193+
.font(.title3)
194+
.foregroundStyle(.secondary)
195+
}
196+
if viewModel.hasFilters {
197+
VStack(spacing: BookingListViewLayout.textVerticalPadding) {
198+
Button("Change filters") {
199+
// TODO
200+
}
201+
.buttonStyle(PrimaryButtonStyle())
202+
Button("Clear filters") {
203+
// TODO
204+
}
205+
}
206+
}
207+
}
208+
Spacer()
209+
}
210+
.multilineTextAlignment(.center)
211+
.padding(.horizontal, BookingListViewLayout.emptyStatePadding)
212+
}
213+
195214
func errorSnackBar(onTap: @escaping () -> Void) -> some View {
196-
Text(Localization.errorMessage)
215+
Text(BookingListViewLocalization.errorMessage)
197216
.foregroundStyle(Color(.listForeground(modal: false)))
198217
.frame(maxWidth: .infinity, alignment: .leading)
199-
.padding(Layout.viewPadding)
218+
.padding(BookingListViewLayout.viewPadding)
200219
.background {
201-
RoundedRectangle(cornerRadius: Layout.cornerRadius)
220+
RoundedRectangle(cornerRadius: BookingListViewLayout.cornerRadius)
202221
.fill(Color(.text))
203222
}
204-
.padding(Layout.viewPadding)
223+
.padding(BookingListViewLayout.viewPadding)
205224
.padding(.bottom, connectivityMonitor.isOffline ? OfflineBannerView.height : 0)
206225
.contentShape(Rectangle())
207226
.onTapGesture { onTap() }
208227
}
209228
}
210229

211-
private extension BookingListView {
212-
enum Layout {
213-
static let textVerticalPadding: CGFloat = 8
214-
static let viewPadding: CGFloat = 16
215-
static let emptyStatePadding: CGFloat = 24
216-
static let emptyStateImageWidth: CGFloat = 67
217-
static let cornerRadius: CGFloat = 8
218-
}
230+
fileprivate enum BookingListViewLayout {
231+
static let textVerticalPadding: CGFloat = 8
232+
static let viewPadding: CGFloat = 16
233+
static let emptyStatePadding: CGFloat = 24
234+
static let emptyStateImageWidth: CGFloat = 67
235+
static let cornerRadius: CGFloat = 8
236+
}
219237

220-
enum Localization {
221-
static let errorMessage = NSLocalizedString(
222-
"bookingList.errorMessage",
223-
value: "Error fetching bookings",
224-
comment: "Error message when fetching bookings fails"
225-
)
226-
static let emptySearchText = NSLocalizedString(
227-
"bookingList.emptySearchText",
228-
value: "We couldn’t find any bookings with that name — try adjusting your search term to see more results.",
229-
comment: "Message displayed when searching bookings by keyword yields no results."
230-
)
231-
}
238+
fileprivate enum BookingListViewLocalization {
239+
static let errorMessage = NSLocalizedString(
240+
"bookingList.errorMessage",
241+
value: "Error fetching bookings",
242+
comment: "Error message when fetching bookings fails"
243+
)
244+
static let emptySearchText = NSLocalizedString(
245+
"bookingList.emptySearchText",
246+
value: "We couldn't find any bookings with that name — try adjusting your search term to see more results.",
247+
comment: "Message displayed when searching bookings by keyword yields no results."
248+
)
232249
}

WooCommerce/Classes/Extensions/Booking+Helpers.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import Foundation
22
import struct Yosemite.Booking
33

44
extension Booking {
5+
var productName: String? {
6+
orderInfo?.productInfo?.name
7+
}
8+
9+
var customerName: String {
10+
guard let name = orderInfo?.customerInfo?.billingAddress.fullName else {
11+
return Localization.guest
12+
}
13+
return name.isEmpty ? Localization.guest : name
14+
}
15+
516
var summaryText: String {
6-
let productName = orderInfo?.productInfo?.name
7-
let customerName: String = {
8-
guard let name = orderInfo?.customerInfo?.billingAddress.fullName else {
9-
return Localization.guest
10-
}
11-
return name.isEmpty ? Localization.guest : name
12-
}()
1317
return [productName, customerName]
1418
.compactMap { $0 }
1519
.joined(separator: "")

WooCommerce/Classes/POS/POSNotificationScheduler.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ import UserNotifications
33
import Yosemite
44
import Experiments
55

6-
// periphery: ignore - work in progress
76
protocol POSNotificationScheduling {
87
func scheduleLocalNotificationIfEligible(for merchantType: POSNotificationScheduler.MerchantType) async
98
}
109

11-
// periphery: ignore - work in progress
1210
final class POSNotificationScheduler: POSNotificationScheduling {
1311
enum MerchantType {
1412
case potentialMerchant
@@ -64,7 +62,7 @@ final class POSNotificationScheduler: POSNotificationScheduling {
6462
func scheduleLocalNotificationIfEligible(for merchantType: POSNotificationScheduler.MerchantType) async {
6563
guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleSurveys) else { return }
6664

67-
let isScheduled = await isNotificationScheduled(for: merchantType)
65+
let isScheduled = await isNotificationAlreadyScheduled(for: merchantType)
6866
guard !isScheduled else { return }
6967
guard isCountryEligible() else { return }
7068

@@ -86,7 +84,23 @@ final class POSNotificationScheduler: POSNotificationScheduling {
8684
}
8785
}
8886

89-
private func isNotificationScheduled(for merchantType: MerchantType) async -> Bool {
87+
private func isNotificationAlreadyScheduled(for merchantType: MerchantType) async -> Bool {
88+
// Check if the specific notification type is already scheduled
89+
let isCurrentMerchantTypeScheduled = await checkIfScheduled(for: merchantType)
90+
if isCurrentMerchantTypeScheduled {
91+
return true
92+
}
93+
94+
// Don't schedule notification for potential merchant if the user is already marked as current merchant
95+
guard merchantType == .potentialMerchant else {
96+
return false
97+
}
98+
99+
let isCurrentMerchantScheduled = await checkIfScheduled(for: .currentMerchant)
100+
return isCurrentMerchantScheduled
101+
}
102+
103+
private func checkIfScheduled(for merchantType: MerchantType) async -> Bool {
90104
await withCheckedContinuation { continuation in
91105
let action: AppSettingsAction
92106
switch merchantType {

WooCommerce/Classes/ViewModels/Booking Details/HeaderContent.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ extension BookingDetailsViewModel {
1111
@Published private(set) var bookingDate: String = ""
1212
@Published private(set) var attendanceStatus: BookingAttendanceStatus = .unknown
1313
@Published private(set) var bookingStatus: BookingStatus = .unknown
14-
@Published private(set) var serviceAndCustomerLine: String = ""
14+
@Published private(set) var serviceLine: String = ""
15+
@Published private(set) var customerLine: String = ""
1516

1617
func update(with booking: Booking) {
1718
bookingDate = booking.startDate.toString(
1819
dateStyle: .short,
1920
timeStyle: .short,
2021
timeZone: BookingListTab.utcTimeZone
2122
)
22-
serviceAndCustomerLine = booking.summaryText
23+
serviceLine = booking.productName ?? ""
24+
customerLine = booking.customerName
2325
attendanceStatus = booking.attendanceStatus
2426
bookingStatus = booking.bookingStatus
2527
}

WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ struct BookingDetailsView: View {
1414
enum Layout {
1515
static let contentSidePadding: CGFloat = 16
1616
static let contentVerticalPadding: CGFloat = 16
17-
static let headerContentVerticalPadding: CGFloat = 6
18-
static let headerBadgesAdditionalTopPadding: CGFloat = 4
17+
static let headerContentVerticalPadding: CGFloat = 2
18+
static let headerBadgesAdditionalTopPadding: CGFloat = 6
1919
static let sectionFooterTextVerticalPadding: CGFloat = 8
2020
static let rowTextVerticalPadding: CGFloat = 11
2121
}

WooCommerce/Classes/ViewRelated/Bookings/Booking Details/HeaderView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,14 @@ extension BookingDetailsView {
1111
.font(TextFont.bodyMedium)
1212
.foregroundColor(.primary)
1313
}
14-
if !content.serviceAndCustomerLine.isEmpty {
15-
Text(content.serviceAndCustomerLine)
16-
.font(.footnote.weight(.medium))
14+
if !content.serviceLine.isEmpty {
15+
Text(content.serviceLine)
16+
.font(.body)
17+
.foregroundColor(.secondary)
18+
}
19+
if !content.customerLine.isEmpty {
20+
Text(content.customerLine)
21+
.font(.body)
1722
.foregroundColor(.secondary)
1823
}
1924
HStack {

WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,9 @@ final class EditableOrderViewModel: ObservableObject {
10441044
guard let self else { return }
10451045
self.collectPayment(for: order)
10461046
self.trackCreateOrderSuccess(usesGiftCard: usesGiftCard)
1047+
Task {
1048+
await self.posNotificationScheduler.scheduleLocalNotificationIfEligible(for: .potentialMerchant)
1049+
}
10471050
} onFailure: { [weak self] error, usesGiftCard in
10481051
guard let self else { return }
10491052
self.fixedNotice = NoticeFactory.createOrderErrorNotice(error, order: self.orderSynchronizer.order)

0 commit comments

Comments
 (0)