Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Woo POS] Coupons: Creation (with dummy entry point UI) #15467

Merged
merged 10 commits into from
Apr 3, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import SwiftUI
import struct Yosemite.Coupon

extension View {
func posCouponCreationSheet(
isPresented: Binding<Bool>,
onSuccess: @escaping () -> Void
) -> some View {
modifier(POSCouponCreationSheetModifier(isPresented: isPresented, onSuccess: onSuccess))
}
}

private struct POSCouponCreationSheetModifier: ViewModifier {
@Binding var isPresented: Bool
let onSuccess: () -> Void

@State private var selectedType: POSCouponDiscountType?
@State private var showCouponSelectionSheet: Bool = false

func body(content: Content) -> some View {
content
.sheet(item: $selectedType) { (posDiscountType: POSCouponDiscountType) in
let viewModel = AddEditCouponViewModel(discountType: posDiscountType.discountType, onSuccess: { _ in })
var view = AddEditCoupon(viewModel)

view.dismissHandler = {
selectedType = nil
}

view.onDisappear = { success in
if success {
selectedType = nil
onSuccess()
}
}

view.discountTypeHandler = { _ in
showCouponSelectionSheet = true
}

return view
.interactiveDismissDisabled()
.discountTypeSelectionSheet(isPresented: $showCouponSelectionSheet) { type in
showCouponSelectionSheet = false
viewModel.discountType = type.discountType
}
}
.discountTypeSelectionSheet(isPresented: $isPresented) { type in
selectedType = type
}
}
}

private extension View {
func discountTypeSelectionSheet(
isPresented: Binding<Bool>,
onSelection: @escaping (POSCouponDiscountType) -> Void
) -> some View {
sheet(isPresented: isPresented) {
let command = DiscountTypeBottomSheetListSelectorCommand(selected: nil) { type in
onSelection(.init(discountType: type))
}

NavigationView {
BottomSheetListSelector(
viewProperties: BottomSheetListSelectorViewProperties(),
command: command,
onDismiss: { _ in
isPresented.wrappedValue = false
}
)
.navigationBarTitleDisplayMode(.large)
.navigationTitle(Localization.selectCouponTypeTitle)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(Localization.selectCouponCancelButtonTitle) {
isPresented.wrappedValue = false
}
}
}
}
.navigationViewStyle(.stack)
.interactiveDismissDisabled()
}
}
}

private struct POSCouponDiscountType: Identifiable, Equatable {
var id: String { discountType.rawValue }
let discountType: Coupon.DiscountType
}

private enum Localization {
static let selectCouponTypeTitle = NSLocalizedString(
"pos.couponCreationSheet.selectCoupon.title",
value: "Create coupon",
comment: "A title for the view that selects the type of coupon to create"
)

static let selectCouponCancelButtonTitle = NSLocalizedString(
"pos.couponCreationSheet.selectCoupon.cancel",
value: "Cancel",
comment: "A button that dismisses coupon creation sheet"
)
}
20 changes: 19 additions & 1 deletion WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import SwiftUI
import enum Yosemite.POSItem
import protocol Yosemite.POSOrderableItem
import struct Yosemite.POSCoupon

@available(iOS 17.0, *)
struct ItemListView: View {
Expand All @@ -23,6 +22,8 @@ struct ItemListView: View {

@State private var selectedItemType: ItemType = .products

@State private var showCouponCreationModal: Bool = false

var body: some View {
if #available(iOS 18.0, *) {
NavigationStack {
Expand Down Expand Up @@ -52,7 +53,19 @@ struct ItemListView: View {
}, label: {
Text("Coupons")
})

Spacer()

Button(action: {
showCouponCreationModal = true
}, label: {
Text(Image(systemName: "plus.circle.fill"))
})
.font(.posButtonSymbolLarge)
.foregroundStyle(Color.posOnSurface)
.renderedIf(selectedItemType == .coupons)
}
.padding(POSPadding.medium)
.renderedIf(shouldShowCoupons)

switch itemListState {
Expand All @@ -76,6 +89,11 @@ struct ItemListView: View {
.posModal(isPresented: $showSimpleProductsModal) {
SimpleProductsOnlyInformation(isPresented: $showSimpleProductsModal)
}
.posCouponCreationSheet(isPresented: $showCouponCreationModal, onSuccess: {
Task {
await posModel.refreshItems(base: .root)
}
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ final class AddEditCouponHostingController: UIHostingController<AddEditCoupon> {

private let viewModel: AddEditCouponViewModel

init(viewModel: AddEditCouponViewModel, onDisappear: @escaping () -> Void) {
init(viewModel: AddEditCouponViewModel, onDisappear: @escaping (_ success: Bool) -> Void) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The success parameter makes sense in the context of this PR, but if I saw it out of the blue it might be tricky to understand what's the success referring to, like "success of disappear"? Would it make sense to rename the param to something like isCouponCreationSuccess, couponCreateResultCallback, or similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. If question like this comes up, then it definetely makes sense to clarify it 👍

self.viewModel = viewModel
super.init(rootView: AddEditCoupon(viewModel))

Expand Down Expand Up @@ -60,7 +60,7 @@ struct AddEditCoupon: View {

/// Set this closure with SwiftUI onDisappear code. Needed because we need to set this event from a UIKit object.
///
var onDisappear: () -> Void = {}
var onDisappear: (_ success: Bool) -> Void = { _ in }

/// Set this closure to display the bottom sheet for discount type selection the UIKit way.
///
Expand Down Expand Up @@ -305,7 +305,7 @@ struct AddEditCoupon: View {
let _ = DDLogError("⛔️ Error acquiring the coupon code after creation")
}
CouponCreationSuccess(couponCode: couponCode, shareMessage: viewModel.shareCouponMessage) {
onDisappear()
onDisappear(true)
}
}
.toolbar {
Expand All @@ -322,7 +322,7 @@ struct AddEditCoupon: View {
}
.navigationViewStyle(.stack)
.onDisappear {
onDisappear()
onDisappear(false)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ final class AddEditCouponViewModel: ObservableObject {

/// Init method for coupon creation
///
init(siteID: Int64,
init(siteID: Int64 = ServiceLocator.stores.sessionManager.defaultStoreID ?? 0,
discountType: Coupon.DiscountType,
stores: StoresManager = ServiceLocator.stores,
storageManager: StorageManagerType = ServiceLocator.storageManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class CouponDetailsHostingController: UIHostingController<CouponDetails> {
guard let self = self else { return }
let addEditHostingController = AddEditCouponHostingController(
viewModel: addEditCouponViewModel,
onDisappear: {
onDisappear: { _ in
viewModel.resetAddEditViewModel()
})
self.present(addEditHostingController, animated: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private extension CouponListViewController {
onSuccess: { [weak self] _ in
self?.refreshCouponList()
})
let addEditHostingController = AddEditCouponHostingController(viewModel: viewModel, onDisappear: { [weak self] in
let addEditHostingController = AddEditCouponHostingController(viewModel: viewModel, onDisappear: { [weak self] _ in
guard let self = self else { return }
self.dismiss(animated: true)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private extension EnhancedCouponListViewController {
onSuccess: { [weak self] _ in
self?.couponListViewController?.refreshCouponList()
})
let addEditHostingController = AddEditCouponHostingController(viewModel: viewModel, onDisappear: { [weak self] in
let addEditHostingController = AddEditCouponHostingController(viewModel: viewModel, onDisappear: { [weak self] _ in
guard let self = self else { return }
self.dismiss(animated: true)
})
Expand Down
12 changes: 12 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
015D99AA2C58C780001D7186 /* PointOfSaleCardPresentPaymentLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015D99A92C58C780001D7186 /* PointOfSaleCardPresentPaymentLayout.swift */; };
01620C4E2C5394B200D3EA2F /* POSProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */; };
01664F9E2C50E685007CB5DD /* POSFontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01664F9D2C50E685007CB5DD /* POSFontStyle.swift */; };
016A77692D9D24B00004FCD6 /* POSCouponCreationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */; };
016C6B972C74AB17000D86FD /* POSConnectivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */; };
0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */; };
0174DDBF2CE600C5005D20CA /* ReceiptEmailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */; };
Expand Down Expand Up @@ -3275,6 +3276,7 @@
015D99A92C58C780001D7186 /* PointOfSaleCardPresentPaymentLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentLayout.swift; sourceTree = "<group>"; };
01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSProgressViewStyle.swift; sourceTree = "<group>"; };
01664F9D2C50E685007CB5DD /* POSFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSFontStyle.swift; sourceTree = "<group>"; };
016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCouponCreationSheet.swift; sourceTree = "<group>"; };
016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = "<group>"; };
0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = "<group>"; };
0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6565,6 +6567,14 @@
path = "Reusable Views";
sourceTree = "<group>";
};
016A77672D9D24A30004FCD6 /* Coupons */ = {
isa = PBXGroup;
children = (
016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */,
);
path = Coupons;
sourceTree = "<group>";
};
0174DDB92CE5FD49005D20CA /* ReceiptEmail */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7241,6 +7251,7 @@
026826A12BF59DED0036F959 /* Presentation */ = {
isa = PBXGroup;
children = (
016A77672D9D24A30004FCD6 /* Coupons */,
026A50262D2F6BBF002C42C2 /* Infinite Scroll */,
029149792D2682DF00F7B3B3 /* Item Selector */,
01620C4C2C5394A400D3EA2F /* Reusable Views */,
Expand Down Expand Up @@ -15817,6 +15828,7 @@
EE9D03122B89DF760077CED1 /* WooAnalyticsEvent+OrdersListFilter.swift in Sources */,
683421642ACE9391009021D7 /* ProductDiscountView.swift in Sources */,
B6E851F3276320C70041D1BA /* RefundCustomAmountsDetailsViewModel.swift in Sources */,
016A77692D9D24B00004FCD6 /* POSCouponCreationSheet.swift in Sources */,
024DF31F23743045006658FE /* Header+AztecFormatting.swift in Sources */,
B5A8F8AD20B88D9900D211DE /* LoginPrologueViewController.swift in Sources */,
B5D1AFC620BC7B7300DB0E8C /* StorePickerViewController.swift in Sources */,
Expand Down
Loading