Skip to content

Commit cebc177

Browse files
authored
Merge branch 'trunk' into woomob-1430-mobile-paymentsios-enable-card-payments-using-stripe-gateway
2 parents d598513 + a696bb2 commit cebc177

File tree

15 files changed

+449
-104
lines changed

15 files changed

+449
-104
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
100100
return buildConfig == .localDeveloper || buildConfig == .alpha
101101
case .pointOfSaleSurveys:
102102
return buildConfig == .localDeveloper || buildConfig == .alpha
103+
case .pointOfSaleSettingsCardReaderFlow:
104+
return buildConfig == .localDeveloper || buildConfig == .alpha
103105
default:
104106
return true
105107
}

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,8 @@ public enum FeatureFlag: Int {
207207
/// Enables surveys for potential and current POS merchants
208208
///
209209
case pointOfSaleSurveys
210+
211+
/// Enables card reader connection flow within POS settings
212+
///
213+
case pointOfSaleSettingsCardReaderFlow
210214
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import SwiftUI
2+
3+
struct POSSettingsCardView: View {
4+
let title: String
5+
let subtitle: String
6+
let action: () -> Void
7+
8+
var body: some View {
9+
Button(action: action) {
10+
VStack(alignment: .leading, spacing: POSPadding.xSmall) {
11+
Text(title)
12+
.font(.posBodyLargeRegular())
13+
.foregroundStyle(Color.posOnSurface)
14+
Text(subtitle)
15+
.font(.posBodyMediumRegular())
16+
.foregroundStyle(.secondary)
17+
}
18+
.padding()
19+
.frame(maxWidth: .infinity, alignment: .leading)
20+
.background(Color.posSurfaceContainerLowest)
21+
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
22+
.posItemCardBorderStyles()
23+
}
24+
.buttonStyle(.plain)
25+
.accessibilityAddTraits(.isButton)
26+
.accessibilityLabel("\(title), \(subtitle)")
27+
}
28+
}
29+
30+
#if DEBUG
31+
#Preview {
32+
POSSettingsCardView(
33+
title: "Documentation",
34+
subtitle: "Learn more about accepting mobile payments",
35+
action: { }
36+
)
37+
}
38+
#endif

Modules/Sources/PointOfSale/Presentation/Settings/PointOfSaleSettingsHardwareDetailView.swift

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import SwiftUI
22
import struct WooFoundation.SafariView
33

44
struct PointOfSaleSettingsHardwareDetailView: View {
5+
@Environment(PointOfSaleAggregateModel.self) private var posModel
6+
@Environment(\.posFeatureFlags) private var featureFlags
57
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
68
@Environment(\.posAnalytics) private var analytics
79

@@ -69,7 +71,12 @@ struct PointOfSaleSettingsHardwareDetailView: View {
6971
.navigationDestination(for: NavigationDestination.self) { destination in
7072
switch destination {
7173
case .hardware(.cardReaders):
72-
cardReadersView
74+
if featureFlags.isFeatureFlagEnabled(.pointOfSaleSettingsCardReaderFlow) {
75+
cardReadersView
76+
} else {
77+
// TODO: Legacy view to be removed along feature flag on WOOMOB-1591
78+
legacyCardReadersView
79+
}
7380
case .hardware(.scanners):
7481
scannersView
7582
}
@@ -92,7 +99,7 @@ struct PointOfSaleSettingsHardwareDetailView: View {
9299
}
93100
}
94101

95-
private var cardReadersView: some View {
102+
private var legacyCardReadersView: some View {
96103
VStack(spacing: POSSpacing.none) {
97104
POSPageHeaderView(
98105
title: Localization.cardReadersTitle,
@@ -161,6 +168,62 @@ struct PointOfSaleSettingsHardwareDetailView: View {
161168
}
162169
}
163170

171+
private var cardReadersView: some View {
172+
VStack(spacing: POSSpacing.none) {
173+
POSPageHeaderView(
174+
title: Localization.cardReadersTitle,
175+
backButtonConfiguration: .init(state: .enabled, action: {
176+
navigationPath.removeLast()
177+
}, buttonIcon: "chevron.left"))
178+
.foregroundColor(.posSurface)
179+
180+
List {
181+
if case .connected = posModel.cardReaderConnectionStatus {
182+
VStack(spacing: POSPadding.xSmall) {
183+
HStack {
184+
Text(Localization.readerModelTitle)
185+
.foregroundStyle(.primary)
186+
Spacer()
187+
Text(cardReaderName)
188+
.foregroundStyle(.secondary)
189+
}
190+
.padding()
191+
HStack {
192+
Text(Localization.readerBatteryTitle)
193+
.foregroundStyle(.primary)
194+
Spacer()
195+
Text(formattedBatteryLevel)
196+
.foregroundStyle(.secondary)
197+
}
198+
.padding()
199+
}
200+
.font(.posBodyMediumRegular())
201+
} else {
202+
POSSettingsCardView(title: Localization.cardReaderConnectTitle,
203+
subtitle: Localization.cardReaderConnectSubtitle,
204+
action: {
205+
posModel.connectCardReader()
206+
})
207+
}
208+
209+
POSSettingsCardView(title: Localization.cardReaderDocumentationTitle,
210+
subtitle: Localization.cardReaderDocumentationSubtitle,
211+
action: { showCardReaderDocumentationModal = true })
212+
.accessibilityAddTraits(.isButton)
213+
.listRowSeparator(.hidden)
214+
}
215+
.listStyle(.plain)
216+
.scrollContentBackground(.hidden)
217+
.listRowBackground(Color.clear)
218+
.background(backgroundColor)
219+
.foregroundColor(.posOnSurface)
220+
}
221+
.navigationBarBackButtonHidden(true)
222+
.posFullScreenCover(isPresented: $showCardReaderDocumentationModal) {
223+
SafariView(url: POSConstants.URLs.inPersonPaymentsLearnMoreWCPay.asURL())
224+
}
225+
}
226+
164227
private var scannersView: some View {
165228
VStack(spacing: POSSpacing.none) {
166229
POSPageHeaderView(
@@ -204,7 +267,8 @@ struct PointOfSaleSettingsHardwareDetailView: View {
204267
}
205268
}
206269

207-
extension PointOfSaleSettingsHardwareDetailView {
270+
// MARK: - Navigation
271+
private extension PointOfSaleSettingsHardwareDetailView {
208272
enum HardwareDestination: Identifiable, CaseIterable {
209273
case cardReaders
210274
case scanners
@@ -381,6 +445,16 @@ private extension PointOfSaleSettingsHardwareDetailView {
381445
value: "Configure barcode scanner settings",
382446
comment: "Description of Barcode scanner settings configuration."
383447
)
448+
static let cardReaderConnectTitle = NSLocalizedString(
449+
"pointOfSaleSettingsHardwareDetailView.cardReaderConnectTitle",
450+
value: "Connect card reader",
451+
comment: "Title for card reader connect button when no reader is connected."
452+
)
453+
static let cardReaderConnectSubtitle = NSLocalizedString(
454+
"pointOfSaleSettingsHardwareDetailView.cardReaderConnectSubtitle",
455+
value: "Connect your card reader and start accepting payments",
456+
comment: "Subtitle for card reader connect button when no reader is connected."
457+
)
384458
}
385459
}
386460

WooCommerce/Classes/Bookings/BookingFilters/BookableProductListSyncable.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Yosemite
55
struct BookableProductListSyncable: ListSyncable {
66
typealias StorageType = StorageProduct
77
typealias ModelType = Product
8+
typealias ListFilterType = BookingProductFilter
89

910
let siteID: Int64
1011

@@ -50,6 +51,10 @@ struct BookableProductListSyncable: ListSyncable {
5051
func displayName(for item: Product) -> String {
5152
item.name
5253
}
54+
55+
func filterItem(for item: Product) -> BookingProductFilter {
56+
BookingProductFilter(productID: item.productID, name: item.name)
57+
}
5358
}
5459

5560
private extension BookableProductListSyncable {

WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,18 @@ final class BookingFiltersViewModel: FilterListViewModel {
3535
}
3636

3737
var criteria: Filters {
38-
let teamMember = teamMemberFilterViewModel.selectedValue as? BookingResource
39-
let product = productFilterViewModel.selectedValue as? BookingProductFilter
38+
let teamMembers = (teamMemberFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingResource] ?? []
39+
let products = (productFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingProductFilter] ?? []
4040
let customer = customerFilterViewModel.selectedValue as? CustomerFilter
41-
let attendanceStatus = attendanceStatusFilterViewModel.selectedValue as? BookingAttendanceStatus
42-
let paymentStatus = paymentStatusFilterViewModel.selectedValue as? BookingStatus
41+
let attendanceStatuses = (attendanceStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingAttendanceStatus] ?? []
42+
let paymentStatuses = (paymentStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingStatus] ?? []
4343
let dateRange = dateTimeFilterViewModel.selectedValue as? BookingDateRangeFilter
4444
let numberOfActiveFilters = filterTypeViewModels.numberOfActiveFilters
4545

46-
return Filters(teamMember: teamMember,
47-
product: product,
48-
attendanceStatus: attendanceStatus,
49-
paymentStatus: paymentStatus,
46+
return Filters(teamMembers: teamMembers,
47+
products: products,
48+
attendanceStatuses: attendanceStatuses,
49+
paymentStatuses: paymentStatuses,
5050
customer: customer,
5151
dateRange: dateRange,
5252
numberOfActiveFilters: numberOfActiveFilters)
@@ -86,55 +86,47 @@ final class BookingFiltersViewModel: FilterListViewModel {
8686

8787
struct Filters: Equatable, HumanReadable {
8888

89-
let teamMember: BookingResource?
90-
let product: BookingProductFilter?
91-
let attendanceStatus: BookingAttendanceStatus?
92-
let paymentStatus: BookingStatus?
89+
let teamMembers: [BookingResource]
90+
let products: [BookingProductFilter]
91+
let attendanceStatuses: [BookingAttendanceStatus]
92+
let paymentStatuses: [BookingStatus]
9393
let customer: CustomerFilter?
9494
let dateRange: BookingDateRangeFilter?
9595

9696
let numberOfActiveFilters: Int
9797

9898
init() {
99-
teamMember = nil
100-
product = nil
101-
attendanceStatus = nil
102-
paymentStatus = nil
99+
teamMembers = []
100+
products = []
101+
attendanceStatuses = []
102+
paymentStatuses = []
103103
customer = nil
104104
dateRange = nil
105105
numberOfActiveFilters = 0
106106
}
107107

108-
init(teamMember: BookingResource?,
109-
product: BookingProductFilter?,
110-
attendanceStatus: BookingAttendanceStatus?,
111-
paymentStatus: BookingStatus?,
108+
init(teamMembers: [BookingResource],
109+
products: [BookingProductFilter],
110+
attendanceStatuses: [BookingAttendanceStatus],
111+
paymentStatuses: [BookingStatus],
112112
customer: CustomerFilter?,
113113
dateRange: BookingDateRangeFilter?,
114114
numberOfActiveFilters: Int) {
115-
self.teamMember = teamMember
116-
self.product = product
117-
self.attendanceStatus = attendanceStatus
118-
self.paymentStatus = paymentStatus
115+
self.teamMembers = teamMembers
116+
self.products = products
117+
self.attendanceStatuses = attendanceStatuses
118+
self.paymentStatuses = paymentStatuses
119119
self.customer = customer
120120
self.dateRange = dateRange
121121
self.numberOfActiveFilters = numberOfActiveFilters
122122
}
123123

124124
var readableString: String {
125-
var readable: [String] = []
126-
if let teamMember {
127-
readable.append(teamMember.name)
128-
}
129-
if let product {
130-
readable.append(product.name)
131-
}
132-
if let attendanceStatus {
133-
readable.append(attendanceStatus.localizedTitle)
134-
}
135-
if let paymentStatus {
136-
readable.append(paymentStatus.localizedTitle)
137-
}
125+
var readable: [String] = teamMembers.map { $0.name } +
126+
products.map { $0.name } +
127+
attendanceStatuses.map { $0.localizedTitle } +
128+
paymentStatuses.map { $0.localizedTitle }
129+
138130
if let customer {
139131
readable.append(customer.description)
140132
}
@@ -185,25 +177,25 @@ extension BookingFiltersViewModel.BookingListFilter {
185177
case .teamMember(let siteID):
186178
return FilterTypeViewModel(title: title,
187179
listSelectorConfig: .bookingResource(siteID: siteID),
188-
selectedValue: filters.teamMember)
180+
selectedValue: MultipleFilterSelection(items: filters.teamMembers))
189181
case .product(let siteID):
190182
return FilterTypeViewModel(title: title,
191183
listSelectorConfig: .bookableProduct(siteID: siteID),
192-
selectedValue: filters.product)
184+
selectedValue: MultipleFilterSelection(items: filters.products))
193185
case .customer(let siteID):
194186
return FilterTypeViewModel(title: title,
195187
listSelectorConfig: .customer(siteID: siteID, source: .booking),
196188
selectedValue: filters.customer)
197189
case .attendanceStatus:
198-
let options: [BookingAttendanceStatus?] = [nil, .booked, .checkedIn, .cancelled, .noShow]
190+
let options: [BookingAttendanceStatus?] = [.booked, .checkedIn, .cancelled, .noShow]
199191
return FilterTypeViewModel(title: title,
200-
listSelectorConfig: .staticOptions(options: options),
201-
selectedValue: filters.attendanceStatus)
192+
listSelectorConfig: .multiSelectStaticOptions(options: options),
193+
selectedValue: MultipleFilterSelection(items: filters.attendanceStatuses))
202194
case .paymentStatus:
203-
let options: [BookingStatus?] = [nil, .complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed]
195+
let options: [BookingStatus?] = [.complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed]
204196
return FilterTypeViewModel(title: title,
205-
listSelectorConfig: .staticOptions(options: options),
206-
selectedValue: filters.paymentStatus)
197+
listSelectorConfig: .multiSelectStaticOptions(options: options),
198+
selectedValue: MultipleFilterSelection(items: filters.paymentStatuses))
207199
case .dateTime:
208200
return FilterTypeViewModel(title: title,
209201
listSelectorConfig: .bookingDateTime,

WooCommerce/Classes/Bookings/BookingFilters/ListSyncable.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Yosemite
66
protocol ListSyncable {
77
associatedtype StorageType: ResultsControllerMutableType
88
associatedtype ModelType: Equatable & Hashable where ModelType == StorageType.ReadOnlyType
9+
associatedtype ListFilterType: FilterType & Equatable
910

1011
var title: String { get }
1112
var emptyStateMessage: String { get }
@@ -27,4 +28,7 @@ protocol ListSyncable {
2728

2829
/// Returns the display name for an item
2930
func displayName(for item: ModelType) -> String
31+
32+
/// Returns the filter type for an item
33+
func filterItem(for item: ModelType) -> ListFilterType
3034
}

0 commit comments

Comments
 (0)