Skip to content

Commit 0d38dd7

Browse files
authored
[Woo POS] Modularization: Extract FormattableAmountTextField, Coupon Creation, ProductImageThumbnail (#16134)
2 parents 5719ed7 + cbe632f commit 0d38dd7

File tree

17 files changed

+273
-128
lines changed

17 files changed

+273
-128
lines changed

Modules/Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ let package = Package(
179179
),
180180
.target(
181181
name: "WooFoundation",
182-
dependencies: ["WooFoundationCore"]
182+
dependencies: [
183+
"WooFoundationCore",
184+
.product(name: "Kingfisher", package: "Kingfisher")
185+
]
183186
),
184187
.target(
185188
name: "WooFoundationCore",
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import SwiftUI
22
import Kingfisher
33

4-
struct ProductImageThumbnail<Placeholder: View>: View {
4+
public struct ProductImageThumbnail<Placeholder: View>: View {
55
private let productImageURL: URL?
66
private let productImageSize: CGFloat
77
private let scale: CGFloat
@@ -25,13 +25,13 @@ struct ProductImageThumbnail<Placeholder: View>: View {
2525
)
2626
}
2727

28-
init(productImageURL: URL?,
29-
productImageSize: CGFloat,
30-
scale: CGFloat,
31-
productImageCornerRadius: CGFloat = 0,
32-
foregroundColor: Color,
33-
cachesOriginalImage: Bool = false,
34-
@ViewBuilder placeholder: () -> Placeholder) {
28+
public init(productImageURL: URL?,
29+
productImageSize: CGFloat,
30+
scale: CGFloat,
31+
productImageCornerRadius: CGFloat = 0,
32+
foregroundColor: Color,
33+
cachesOriginalImage: Bool = false,
34+
@ViewBuilder placeholder: () -> Placeholder) {
3535
self.productImageURL = productImageURL
3636
self.productImageSize = productImageSize
3737
self.scale = scale
@@ -41,7 +41,7 @@ struct ProductImageThumbnail<Placeholder: View>: View {
4141
self.placeholder = placeholder()
4242
}
4343

44-
var body: some View {
44+
public var body: some View {
4545
KFImage
4646
.url(productImageURL)
4747
.cacheOriginalImage(cachesOriginalImage)
@@ -57,22 +57,3 @@ struct ProductImageThumbnail<Placeholder: View>: View {
5757
.accessibilityHidden(true)
5858
}
5959
}
60-
61-
// Convenience initializer that maintains the default behavior.
62-
extension ProductImageThumbnail where Placeholder == Image {
63-
init(productImageURL: URL?,
64-
productImageSize: CGFloat,
65-
scale: CGFloat,
66-
productImageCornerRadius: CGFloat = 0,
67-
foregroundColor: Color,
68-
cachesOriginalImage: Bool = false) {
69-
self.init(productImageURL: productImageURL,
70-
productImageSize: productImageSize,
71-
scale: scale,
72-
productImageCornerRadius: productImageCornerRadius,
73-
foregroundColor: foregroundColor,
74-
cachesOriginalImage: cachesOriginalImage) {
75-
Image(uiImage: .productPlaceholderImage)
76-
}
77-
}
78-
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
import struct WooFoundation.ProductImageThumbnail
3+
4+
// Convenience initializer that maintains the default behavior.
5+
extension ProductImageThumbnail where Placeholder == Image {
6+
init(productImageURL: URL?,
7+
productImageSize: CGFloat,
8+
scale: CGFloat,
9+
productImageCornerRadius: CGFloat = 0,
10+
foregroundColor: Color,
11+
cachesOriginalImage: Bool = false) {
12+
self.init(productImageURL: productImageURL,
13+
productImageSize: productImageSize,
14+
scale: scale,
15+
productImageCornerRadius: productImageCornerRadius,
16+
foregroundColor: foregroundColor,
17+
cachesOriginalImage: cachesOriginalImage) {
18+
Image(uiImage: .productPlaceholderImage)
19+
}
20+
}
21+
}

WooCommerce/Classes/POS/Adaptors/POSServiceLocatorAdaptor.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import Yosemite
55
import protocol Experiments.FeatureFlagService
66
import enum Experiments.FeatureFlag
77
import protocol Storage.StorageManagerType
8-
98
final class POSServiceLocatorAdaptor: POSDependencyProviding {
109
var analytics: POSAnalyticsProviding {
1110
POSAnalyticsAdaptor()
@@ -84,4 +83,36 @@ private struct POSExternalViewAdaptor: POSExternalViewProviding {
8483
defaultSite: ServiceLocator.stores.sessionManager.defaultSite))
8584
)
8685
}
86+
87+
func createFormattableAmountTextField(preset: Decimal?,
88+
onSubmit: @escaping () -> Void,
89+
onChange: @escaping (String) -> Void) -> AnyView {
90+
AnyView(POSFormattableAmountTextFieldAdaptor(preset: preset, onSubmit: onSubmit, onChange: onChange))
91+
}
92+
93+
func createCouponCreationView(discountType: Coupon.DiscountType,
94+
showTypeSelection: Binding<Bool>,
95+
onSuccess: @escaping (Coupon) -> Void,
96+
dismissHandler: @escaping () -> Void,
97+
onDisappear: @escaping () -> Void) -> AnyView {
98+
AnyView(POSCouponCreationViewAdaptor(
99+
discountType: discountType,
100+
showTypeSelection: showTypeSelection,
101+
onSuccess: onSuccess,
102+
dismissHandler: dismissHandler,
103+
onDisappear: onDisappear
104+
))
105+
}
106+
107+
func createDiscountTypeSelectionSheet(isPresented: Binding<Bool>,
108+
title: String,
109+
cancelButtonTitle: String,
110+
onSelection: @escaping (Coupon.DiscountType) -> Void) -> AnyView {
111+
AnyView(POSDiscountTypeSelectionSheetAdaptor(
112+
isPresented: isPresented,
113+
title: title,
114+
cancelButtonTitle: cancelButtonTitle,
115+
onSelection: onSelection
116+
))
117+
}
87118
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SwiftUI
2+
import struct Yosemite.Coupon
3+
4+
/// Provide access to AddEditCouponView and ViewModel for POS without making it as an explicit type dependency
5+
/// AddEditCouponView cannot be easily moved and reused in a shared module due to multiple dependencies
6+
/// This is used as a workaround to enable POS modularization without requiring a larger refactoring effort
7+
///
8+
9+
struct POSCouponCreationViewAdaptor: View {
10+
@StateObject private var viewModel: AddEditCouponViewModel
11+
@Binding private var showTypeSelection: Bool
12+
private let dismissHandler: () -> Void
13+
private let onDisappear: () -> Void
14+
15+
init(discountType: Coupon.DiscountType,
16+
showTypeSelection: Binding<Bool>,
17+
onSuccess: @escaping (Coupon) -> Void,
18+
dismissHandler: @escaping () -> Void,
19+
onDisappear: @escaping () -> Void) {
20+
_showTypeSelection = showTypeSelection
21+
self.dismissHandler = dismissHandler
22+
self.onDisappear = onDisappear
23+
_viewModel = StateObject(wrappedValue: AddEditCouponViewModel(
24+
discountType: discountType,
25+
onSuccess: onSuccess
26+
))
27+
}
28+
29+
var body: some View {
30+
var view = AddEditCoupon(viewModel)
31+
view.dismissHandler = dismissHandler
32+
view.onDisappear = onDisappear
33+
view.discountTypeHandler = { _ in
34+
showTypeSelection = true
35+
}
36+
37+
return view
38+
.interactiveDismissDisabled()
39+
}
40+
}
41+
42+
struct POSDiscountTypeSelectionSheetAdaptor: View {
43+
@Binding var isPresented: Bool
44+
let title: String
45+
let cancelButtonTitle: String
46+
let onSelection: (Coupon.DiscountType) -> Void
47+
48+
var body: some View {
49+
let command = DiscountTypeBottomSheetListSelectorCommand(selected: nil) { type in
50+
onSelection(type)
51+
}
52+
53+
NavigationView {
54+
BottomSheetListSelector(
55+
viewProperties: BottomSheetListSelectorViewProperties(),
56+
command: command,
57+
onDismiss: { _ in
58+
isPresented = false
59+
}
60+
)
61+
.navigationBarTitleDisplayMode(.large)
62+
.navigationTitle(title)
63+
.toolbar {
64+
ToolbarItem(placement: .cancellationAction) {
65+
Button(cancelButtonTitle) {
66+
isPresented = false
67+
}
68+
}
69+
}
70+
}
71+
.navigationViewStyle(.stack)
72+
.interactiveDismissDisabled()
73+
}
74+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftUI
2+
3+
/// Provide access to FormattableAmountTextField and ViewModel for POS without making it as an explicit type dependency
4+
/// FormattableAmountTextField cannot be easily moved and reused in a shared module due to multiple dependencies
5+
/// This is used as a workaround to enable POS modularization without requiring a larger refactoring effort
6+
///
7+
struct POSFormattableAmountTextFieldAdaptor: View {
8+
@StateObject private var textFieldViewModel: FormattableAmountTextFieldViewModel
9+
let preset: Decimal?
10+
let onSubmit: () -> Void
11+
let onChange: (String) -> Void
12+
13+
init(preset: Decimal?, onSubmit: @escaping () -> Void, onChange: @escaping (String) -> Void) {
14+
self._textFieldViewModel = StateObject(wrappedValue: FormattableAmountTextFieldViewModel(
15+
size: .extraLarge,
16+
locale: Locale.autoupdatingCurrent,
17+
storeCurrencySettings: ServiceLocator.currencySettings,
18+
allowNegativeNumber: false)
19+
)
20+
self.preset = preset
21+
self.onSubmit = onSubmit
22+
self.onChange = onChange
23+
}
24+
25+
var body: some View {
26+
FormattableAmountTextField(viewModel: textFieldViewModel, style: .pos)
27+
.dynamicTypeSize(...DynamicTypeSize.accessibility1)
28+
.onSubmit {
29+
onSubmit()
30+
}
31+
.onChange(of: textFieldViewModel.amount) { _, newValue in
32+
onChange(newValue)
33+
}
34+
.onAppear {
35+
if let preset {
36+
textFieldViewModel.presetAmount(preset)
37+
}
38+
}
39+
}
40+
}

WooCommerce/Classes/POS/Presentation/Coupons/POSCouponCreationSheet.swift

Lines changed: 19 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ private struct POSCouponCreationSheetModifier: ViewModifier {
1616
@Binding var isPresented: Bool
1717
let onSuccess: (POSItem) -> Void
1818

19+
@Environment(\.posExternalViews) private var externalViews
1920
@State private var selectedType: POSCouponDiscountType?
2021
@State private var showCouponSelectionSheet: Bool = false
2122
@State private var addedCouponItem: POSItem?
2223

2324
func body(content: Content) -> some View {
2425
content
2526
.posSheet(item: $selectedType) { (posDiscountType: POSCouponDiscountType) in
26-
POSCouponCreationView(
27+
externalViews.createCouponCreationView(
2728
discountType: posDiscountType.discountType,
2829
showTypeSelection: $showCouponSelectionSheet,
2930
onSuccess: { coupon in
@@ -40,81 +41,26 @@ private struct POSCouponCreationSheetModifier: ViewModifier {
4041
}
4142
}
4243
)
43-
}
44-
.discountTypeSelectionSheet(isPresented: $isPresented) { type in
45-
selectedType = type
46-
}
47-
}
48-
}
49-
50-
private struct POSCouponCreationView: View {
51-
@StateObject private var viewModel: AddEditCouponViewModel
52-
@Binding var showTypeSelection: Bool
53-
let dismissHandler: () -> Void
54-
let onDisappear: () -> Void
55-
56-
init(discountType: Coupon.DiscountType,
57-
showTypeSelection: Binding<Bool>,
58-
onSuccess: @escaping (Coupon) -> Void,
59-
dismissHandler: @escaping () -> Void,
60-
onDisappear: @escaping () -> Void) {
61-
_showTypeSelection = showTypeSelection
62-
self.dismissHandler = dismissHandler
63-
self.onDisappear = onDisappear
64-
_viewModel = StateObject(wrappedValue: AddEditCouponViewModel(
65-
discountType: discountType,
66-
onSuccess: onSuccess
67-
))
68-
}
69-
70-
var body: some View {
71-
var view = AddEditCoupon(viewModel)
72-
view.dismissHandler = dismissHandler
73-
view.onDisappear = onDisappear
74-
view.discountTypeHandler = { _ in
75-
showTypeSelection = true
76-
}
77-
78-
return view
79-
.interactiveDismissDisabled()
80-
.discountTypeSelectionSheet(isPresented: $showTypeSelection) { type in
81-
showTypeSelection = false
82-
viewModel.discountType = type.discountType
83-
}
84-
}
85-
}
86-
87-
private extension View {
88-
func discountTypeSelectionSheet(
89-
isPresented: Binding<Bool>,
90-
onSelection: @escaping (POSCouponDiscountType) -> Void
91-
) -> some View {
92-
posSheet(isPresented: isPresented) {
93-
let command = DiscountTypeBottomSheetListSelectorCommand(selected: nil) { type in
94-
onSelection(.init(discountType: type))
95-
}
96-
97-
NavigationView {
98-
BottomSheetListSelector(
99-
viewProperties: BottomSheetListSelectorViewProperties(),
100-
command: command,
101-
onDismiss: { _ in
102-
isPresented.wrappedValue = false
103-
}
104-
)
105-
.navigationBarTitleDisplayMode(.large)
106-
.navigationTitle(Localization.selectCouponTypeTitle)
107-
.toolbar {
108-
ToolbarItem(placement: .cancellationAction) {
109-
Button(Localization.selectCouponCancelButtonTitle) {
110-
isPresented.wrappedValue = false
111-
}
44+
.posSheet(isPresented: $showCouponSelectionSheet) {
45+
externalViews.createDiscountTypeSelectionSheet(
46+
isPresented: $showCouponSelectionSheet,
47+
title: Localization.selectCouponTypeTitle,
48+
cancelButtonTitle: Localization.selectCouponCancelButtonTitle
49+
) { type in
50+
showCouponSelectionSheet = false
51+
selectedType = POSCouponDiscountType(discountType: type)
11252
}
11353
}
11454
}
115-
.navigationViewStyle(.stack)
116-
.interactiveDismissDisabled()
117-
}
55+
.posSheet(isPresented: $isPresented) {
56+
externalViews.createDiscountTypeSelectionSheet(
57+
isPresented: $isPresented,
58+
title: Localization.selectCouponTypeTitle,
59+
cancelButtonTitle: Localization.selectCouponCancelButtonTitle
60+
) { type in
61+
selectedType = POSCouponDiscountType(discountType: type)
62+
}
63+
}
11864
}
11965
}
12066

0 commit comments

Comments
 (0)