From 4c9e9615a74a696b00762f38e3d056823635559a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 17 Jul 2023 18:55:24 +0200 Subject: [PATCH 01/41] Add feature list button to plan view and initial data --- .../Upgrades/OwnerUpgradesView.swift | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index 01fe8181b71..e6645cf566c 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -2,6 +2,57 @@ import SwiftUI import Yosemite import WooFoundation +struct FullFeatureListGroups { + public let title: String + public let essentialFeatures: [String] + public let performanceFeatures: [String] +} + +struct FullFeatureListViewModel { + static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { + return [ + FullFeatureListGroups(title: "Your Store", + essentialFeatures: [ + "WooCommerce store", + "WooCommerce mobile app", + "WordPress CMS", + "WordPress mobile app" + ], performanceFeatures: [] + ), + FullFeatureListGroups(title: "Products", + essentialFeatures: [ + "List unlimited products", + "Gift cards", + ], performanceFeatures: [ + "Min/Max order quantity" + ]) + ] + } +} + +struct FullFeatureListView: View { + var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() + var body: some View { + ScrollView() { + ForEach(featureListGroups, id: \.title) { featureList in + Text(featureList.title) + .font(.title) + .bold() + ForEach(featureList.essentialFeatures, id: \.self) { feature in + Text(feature) + .font(.body) + } + ForEach(featureList.performanceFeatures, id: \.self) { feature in + Text(feature) + .font(.body) + .underline(color: .red) + } + Divider() + } + } + } +} + struct OwnerUpgradesView: View { @State var upgradePlans: [WooWPComPlan] @State var isPurchasing: Bool @@ -22,6 +73,7 @@ struct OwnerUpgradesView: View { private var paymentFrequencies: [LegacyWooPlan.PlanFrequency] = [.year, .month] @State var selectedPlan: WooWPComPlan? = nil + @State private var showingFullFeatureList = false var body: some View { VStack(spacing: 0) { @@ -47,6 +99,11 @@ struct OwnerUpgradesView: View { .shimmering(active: isLoading) .padding(.bottom, 8) } + Button(Localization.allFeaturesListText) { + showingFullFeatureList.toggle() + }.sheet(isPresented: $showingFullFeatureList) { + FullFeatureListView() + } } .padding() } @@ -95,6 +152,10 @@ private extension OwnerUpgradesView { static let selectPlanButtonText = NSLocalizedString( "Select a plan", comment: "The title of the button to purchase a Plan when no plan is selected yet.") + + static let allFeaturesListText = NSLocalizedString( + "View Full Feature List", + comment: "The title of the button to view a list of all features that plans offer.") } } From 18a7d44a76156e32e60f34d11ca3f22371f8f97b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Mon, 17 Jul 2023 19:23:52 +0200 Subject: [PATCH 02/41] Added full feature list and initial UI styling --- .../Upgrades/OwnerUpgradesView.swift | 107 +++++++++++++++--- 1 file changed, 89 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index e6645cf566c..8e5326614ee 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -16,16 +16,67 @@ struct FullFeatureListViewModel { "WooCommerce store", "WooCommerce mobile app", "WordPress CMS", - "WordPress mobile app" - ], performanceFeatures: [] + "WordPress mobile app", + "Free SSL certificate", + "Generous storage", + "Automated backup + quick restore", + "Ad-free experience", + "Unlimited admin accounts", + "Live chat support", + "Email support", + "Premium themes included", + "Sales reports", + "Google Analytics" + ], + performanceFeatures: [] ), FullFeatureListGroups(title: "Products", essentialFeatures: [ "List unlimited products", "Gift cards", - ], performanceFeatures: [ - "Min/Max order quantity" - ]) + ], + performanceFeatures: [ + "Min/Max order quantity", + "Product Bundles", + "Custom product kits", + "List products by brand", + "Product recommendations" + ]), + FullFeatureListGroups(title: "Payments", + essentialFeatures: [ + "Integrated payments", + "International payments'", + "Automated sales taxes", + "Accept local payments'", + "Recurring payments'" + ], + performanceFeatures: []), + + FullFeatureListGroups(title: "Marketing & Email", + essentialFeatures: [ + "Promote on TikTok", + "Sync with Pinterest", + "Connect with Facebook", + "Advanced SEO tools", + "Advertise on Google", + "Custom order emails", + ], + performanceFeatures: [ + "Back in stock emails", + "Marketing automation", + "Abandoned cart recovery", + "Referral programs", + "Customer birthday emails", + "Loyalty points programs" + ]), + + FullFeatureListGroups(title: "Shipping", + essentialFeatures: [ + "Shipment tracking", + "Live shipping rates", + "Discounted shipping²", + "Print shipping labels²"], + performanceFeatures: []), ] } } @@ -34,25 +85,45 @@ struct FullFeatureListView: View { var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() var body: some View { ScrollView() { - ForEach(featureListGroups, id: \.title) { featureList in - Text(featureList.title) - .font(.title) - .bold() - ForEach(featureList.essentialFeatures, id: \.self) { feature in - Text(feature) - .font(.body) - } - ForEach(featureList.performanceFeatures, id: \.self) { feature in - Text(feature) - .font(.body) - .underline(color: .red) + VStack(alignment: .leading, spacing: 8.0) { + ForEach(featureListGroups, id: \.title) { featureList in + Text(featureList.title) + .font(.title) + .bold() + ForEach(featureList.essentialFeatures, id: \.self) { feature in + Text(feature) + .font(.body) + } + ForEach(featureList.performanceFeatures, id: \.self) { feature in + Text(feature) + .font(.body) + .underline(color: .red) + } + Divider() } - Divider() + Text(Localization.disclaimer1) + .font(.caption) + Text(Localization.disclaimer2) + .font(.caption) } + .background(Color(.listBackground)) + .navigationTitle("Full Feature List") } } } +private extension FullFeatureListView { + struct Localization { + static let disclaimer1 = NSLocalizedString( + "1. Available as standard in WooCommerce Payments (restrictions apply)." + + "Additional extensions may be required for other payment providers." , + comment: "") + static let disclaimer2 = NSLocalizedString( + "2. Only available in the U.S. – an additional extension will be required for other countries.", + comment: "") + } +} + struct OwnerUpgradesView: View { @State var upgradePlans: [WooWPComPlan] @State var isPurchasing: Bool From c49f5b233c528d3c02fa3b7a58a26c3efda96e1d Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 09:49:53 +0200 Subject: [PATCH 03/41] Add navigation bar, and dismiss. sfsymbols to features --- .../Upgrades/OwnerUpgradesView.swift | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index 8e5326614ee..3dd38f1911a 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -82,7 +82,10 @@ struct FullFeatureListViewModel { } struct FullFeatureListView: View { + @Environment(\.presentationMode) var presentationMode + var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() + var body: some View { ScrollView() { VStack(alignment: .leading, spacing: 8.0) { @@ -95,9 +98,13 @@ struct FullFeatureListView: View { .font(.body) } ForEach(featureList.performanceFeatures, id: \.self) { feature in - Text(feature) - .font(.body) - .underline(color: .red) + HStack { + Text(feature) + .font(.body) + Image(systemName: "star.fill") + .foregroundColor(.purple) + .font(.footnote) + } } Divider() } @@ -106,9 +113,15 @@ struct FullFeatureListView: View { Text(Localization.disclaimer2) .font(.caption) } - .background(Color(.listBackground)) - .navigationTitle("Full Feature List") + .padding(.horizontal) } + .navigationTitle("Full Feature List") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Image(systemName: "chevron.left") + }) } } @@ -172,8 +185,12 @@ struct OwnerUpgradesView: View { } Button(Localization.allFeaturesListText) { showingFullFeatureList.toggle() - }.sheet(isPresented: $showingFullFeatureList) { - FullFeatureListView() + } + .buttonStyle(SecondaryButtonStyle()) + .sheet(isPresented: $showingFullFeatureList) { + NavigationView { + FullFeatureListView() + } } } .padding() From 3ed23a6bf4696c15a98a6789bce0e8cd597894b7 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 09:55:18 +0200 Subject: [PATCH 04/41] Update background card and sfsymbol colors --- .../Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift | 2 +- WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index 3dd38f1911a..d9fd9b546cf 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -102,7 +102,7 @@ struct FullFeatureListView: View { Text(feature) .font(.body) Image(systemName: "star.fill") - .foregroundColor(.purple) + .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) .font(.footnote) } } diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift index 6b134decf78..525c384fca2 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift @@ -51,7 +51,7 @@ struct WooPlanCardView: View { .frame(maxWidth: .infinity) .padding(.horizontal) .padding(.vertical) - .background(Color(.systemGroupedBackground)) + .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(Layout.cornerRadius) .overlay( RoundedRectangle(cornerRadius: Layout.cornerRadius) From 7b03b1c4542c56767b8ff358f5490a2ba9fef8e6 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 10:22:26 +0200 Subject: [PATCH 05/41] Add vertical padding between components --- .../ViewRelated/Upgrades/OwnerUpgradesView.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index d9fd9b546cf..eaa035b4c99 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -93,6 +93,7 @@ struct FullFeatureListView: View { Text(featureList.title) .font(.title) .bold() + .padding(.bottom) ForEach(featureList.essentialFeatures, id: \.self) { feature in Text(feature) .font(.body) @@ -107,6 +108,15 @@ struct FullFeatureListView: View { } } Divider() + .padding(.bottom) + HStack { + Image(systemName: "star.fill") + Text("Performance plan only") + } + .font(.footnote) + .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) + .padding(.bottom) + .renderedIf(featureList.performanceFeatures.isNotEmpty) } Text(Localization.disclaimer1) .font(.caption) @@ -115,6 +125,7 @@ struct FullFeatureListView: View { } .padding(.horizontal) } + .padding() .navigationTitle("Full Feature List") .navigationBarTitleDisplayMode(.inline) .navigationBarItems(leading: Button(action: { @@ -132,8 +143,8 @@ private extension FullFeatureListView { "Additional extensions may be required for other payment providers." , comment: "") static let disclaimer2 = NSLocalizedString( - "2. Only available in the U.S. – an additional extension will be required for other countries.", - comment: "") + "2. Only available in the U.S. – an additional extension will be required for other countries.", + comment: "") } } From 2a86b3274a741a6c58d333a21f7013719be26fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 10:44:17 +0200 Subject: [PATCH 06/41] Link Coupon List from Order Details --- .../ViewRelated/Coupons/CouponListView.swift | 38 +++++++++++++++++++ .../EditableOrderViewModel.swift | 2 + .../PaymentSection/OrderPaymentSection.swift | 2 +- .../WooCommerce.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift new file mode 100644 index 00000000000..ec9c4334bc1 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct CouponListView: UIViewControllerRepresentable { + let siteID: Int64 + + typealias UIViewControllerType = CouponListViewController + + class Coordinator { + var parentObserver: NSKeyValueObservation? + var rightBarButtonItemsObserver: NSKeyValueObservation? + } + + /// This is a UIKit solution for fixing Navigation Title and Bar Button Items ignored in NavigationView. + /// This solution doesn't require making internal changes to the destination `UIViewController` + /// and should be called once, when wrapped. + /// Solution proposed here: https://stackoverflow.com/a/68567095/7241994 + /// + func makeUIViewController(context: Self.Context) -> CouponListViewController { + let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) + context.coordinator.parentObserver = viewController.observe(\.parent, changeHandler: { vc, _ in + vc.parent?.navigationItem.title = vc.title + vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems + }) + + // This fixes the issue when `rightBarButtonItem` is updated in `CouponListViewController`, + // the hosting controller should be updated to reflect the change. + context.coordinator.rightBarButtonItemsObserver = viewController.observe(\.navigationItem.rightBarButtonItems, changeHandler: { vc, _ in + vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems + }) + return viewController + } + + func updateUIViewController(_ uiViewController: CouponListViewController, context: Context) { + // nothing to do here + } + + func makeCoordinator() -> Self.Coordinator { Coordinator() } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift index b506310aa90..9978f3d395f 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift @@ -663,6 +663,7 @@ extension EditableOrderViewModel { /// Representation of payment data display properties /// struct PaymentDataViewModel { + let siteID: Int64 let itemsTotal: String let orderTotal: String @@ -723,6 +724,7 @@ extension EditableOrderViewModel { saveFeeLineClosure: @escaping (String?) -> Void = { _ in }, saveCouponLineClosure: @escaping (CouponLineDetailsResult) -> Void = { _ in }, currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) { + self.siteID = siteID self.itemsTotal = currencyFormatter.formatAmount(itemsTotal) ?? "0.00" self.shouldShowShippingTotal = shouldShowShippingTotal self.shippingTotal = currencyFormatter.formatAmount(shippingTotal) ?? "0.00" diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 07de2784b5c..23deaa4fe45 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -71,7 +71,7 @@ struct OrderPaymentSection: View { addCouponRow .sheet(isPresented: $shouldShowAddCouponLineDetails) { - CouponLineDetails(viewModel: viewModel.addCouponLineViewModel) + CouponListView(siteID: viewModel.siteID) } TitleAndValueRow(title: Localization.taxesTotal, value: .content(viewModel.taxesTotal)) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7bfb3073670..659367ca708 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1640,6 +1640,7 @@ B95112DA28BF79CA00D9578D /* PaymentsRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95112D928BF79CA00D9578D /* PaymentsRoute.swift */; }; B95864082A657D2F002C4C6E /* EnhancedCouponListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95864072A657D2F002C4C6E /* EnhancedCouponListViewController.swift */; }; B958640A2A657F44002C4C6E /* EnhancedCouponListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95864092A657F44002C4C6E /* EnhancedCouponListView.swift */; }; + B958640C2A66847B002C4C6E /* CouponListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958640B2A66847B002C4C6E /* CouponListView.swift */; }; B958A7C728B3D44A00823EEF /* UniversalLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958A7C628B3D44A00823EEF /* UniversalLinkRouter.swift */; }; B958A7C928B3D47B00823EEF /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958A7C828B3D47B00823EEF /* Route.swift */; }; B958A7CB28B3D4A100823EEF /* RouteMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958A7CA28B3D4A100823EEF /* RouteMatcher.swift */; }; @@ -4030,6 +4031,7 @@ B95112D928BF79CA00D9578D /* PaymentsRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsRoute.swift; sourceTree = ""; }; B95864072A657D2F002C4C6E /* EnhancedCouponListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedCouponListViewController.swift; sourceTree = ""; }; B95864092A657F44002C4C6E /* EnhancedCouponListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedCouponListView.swift; sourceTree = ""; }; + B958640B2A66847B002C4C6E /* CouponListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponListView.swift; sourceTree = ""; }; B958A7C628B3D44A00823EEF /* UniversalLinkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinkRouter.swift; sourceTree = ""; }; B958A7C828B3D47B00823EEF /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; }; B958A7CA28B3D4A100823EEF /* RouteMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteMatcher.swift; sourceTree = ""; }; @@ -6204,6 +6206,7 @@ DE7B478F27A153C20018742E /* CouponSearchUICommand.swift */, B95864072A657D2F002C4C6E /* EnhancedCouponListViewController.swift */, B95864092A657F44002C4C6E /* EnhancedCouponListView.swift */, + B958640B2A66847B002C4C6E /* CouponListView.swift */, ); path = Coupons; sourceTree = ""; @@ -12046,6 +12049,7 @@ DE6906E527D7439C00735E3B /* OrdersSplitViewWrapperController.swift in Sources */, AEC12B7A2758D55900845F97 /* OrderStatusList.swift in Sources */, 0230535B2374FB6800487A64 /* AztecSourceCodeFormatBarCommand.swift in Sources */, + B958640C2A66847B002C4C6E /* CouponListView.swift in Sources */, D41C9F2E26D9A0E900993558 /* WhatsNewViewModel.swift in Sources */, 02ECD1E424FF5E0B00735BE5 /* AddProductCoordinator.swift in Sources */, 45381B4527341B8A003FEC5F /* DateRangeFilterViewController.swift in Sources */, From 3c5e3642af4211d9a4534542dbfafc93c1fcfe02 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 11:17:59 +0200 Subject: [PATCH 07/41] Extract FullFeatureListView to a separate file --- .../Upgrades/FullFeatureListView.swift | 151 ++++++++++++++++++ .../Upgrades/OwnerUpgradesView.swift | 146 ----------------- .../WooCommerce.xcodeproj/project.pbxproj | 4 + 3 files changed, 155 insertions(+), 146 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift new file mode 100644 index 00000000000..09dd22a88ad --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -0,0 +1,151 @@ +import Foundation +import SwiftUI + +struct FullFeatureListGroups { + public let title: String + public let essentialFeatures: [String] + public let performanceFeatures: [String] +} + +struct FullFeatureListViewModel { + static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { + return [ + FullFeatureListGroups(title: "Your Store", + essentialFeatures: [ + "WooCommerce store", + "WooCommerce mobile app", + "WordPress CMS", + "WordPress mobile app", + "Free SSL certificate", + "Generous storage", + "Automated backup + quick restore", + "Ad-free experience", + "Unlimited admin accounts", + "Live chat support", + "Email support", + "Premium themes included", + "Sales reports", + "Google Analytics" + ], + performanceFeatures: [] + ), + FullFeatureListGroups(title: "Products", + essentialFeatures: [ + "List unlimited products", + "Gift cards", + ], + performanceFeatures: [ + "Min/Max order quantity", + "Product Bundles", + "Custom product kits", + "List products by brand", + "Product recommendations" + ]), + FullFeatureListGroups(title: "Payments", + essentialFeatures: [ + "Integrated payments", + "International payments'", + "Automated sales taxes", + "Accept local payments'", + "Recurring payments'" + ], + performanceFeatures: []), + + FullFeatureListGroups(title: "Marketing & Email", + essentialFeatures: [ + "Promote on TikTok", + "Sync with Pinterest", + "Connect with Facebook", + "Advanced SEO tools", + "Advertise on Google", + "Custom order emails", + ], + performanceFeatures: [ + "Back in stock emails", + "Marketing automation", + "Abandoned cart recovery", + "Referral programs", + "Customer birthday emails", + "Loyalty points programs" + ]), + + FullFeatureListGroups(title: "Shipping", + essentialFeatures: [ + "Shipment tracking", + "Live shipping rates", + "Discounted shipping²", + "Print shipping labels²"], + performanceFeatures: []), + ] + } +} + +struct FullFeatureListView: View { + @Environment(\.presentationMode) var presentationMode + + var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() + + var body: some View { + ScrollView() { + VStack(alignment: .leading, spacing: 8.0) { + ForEach(featureListGroups, id: \.title) { featureList in + Text(featureList.title) + .font(.title) + .bold() + .padding(.bottom) + ForEach(featureList.essentialFeatures, id: \.self) { feature in + Text(feature) + .font(.body) + } + ForEach(featureList.performanceFeatures, id: \.self) { feature in + HStack { + Text(feature) + .font(.body) + Image(systemName: "star.fill") + .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) + .font(.footnote) + } + } + Divider() + .padding(.bottom) + HStack { + Image(systemName: "star.fill") + Text("Performance plan only") + } + .font(.footnote) + .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) + .padding(.bottom) + .renderedIf(featureList.performanceFeatures.isNotEmpty) + } + // Outside of the loop + VStack { + Text(Localization.disclaimer1) + .font(.caption) + Text(Localization.disclaimer2) + .font(.caption) + } + } + .padding(.horizontal) + } + .padding() + .navigationTitle("Full Feature List") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Image(systemName: "chevron.left") + }) + } +} + +private extension FullFeatureListView { + struct Localization { + static let disclaimer1 = NSLocalizedString( + "1. Available as standard in WooCommerce Payments (restrictions apply)." + + "Additional extensions may be required for other payment providers." , + comment: "") + static let disclaimer2 = NSLocalizedString( + "2. Only available in the U.S. – an additional extension will be required for other countries.", + comment: "") + } +} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift index eaa035b4c99..8e4775ec287 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift @@ -2,152 +2,6 @@ import SwiftUI import Yosemite import WooFoundation -struct FullFeatureListGroups { - public let title: String - public let essentialFeatures: [String] - public let performanceFeatures: [String] -} - -struct FullFeatureListViewModel { - static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { - return [ - FullFeatureListGroups(title: "Your Store", - essentialFeatures: [ - "WooCommerce store", - "WooCommerce mobile app", - "WordPress CMS", - "WordPress mobile app", - "Free SSL certificate", - "Generous storage", - "Automated backup + quick restore", - "Ad-free experience", - "Unlimited admin accounts", - "Live chat support", - "Email support", - "Premium themes included", - "Sales reports", - "Google Analytics" - ], - performanceFeatures: [] - ), - FullFeatureListGroups(title: "Products", - essentialFeatures: [ - "List unlimited products", - "Gift cards", - ], - performanceFeatures: [ - "Min/Max order quantity", - "Product Bundles", - "Custom product kits", - "List products by brand", - "Product recommendations" - ]), - FullFeatureListGroups(title: "Payments", - essentialFeatures: [ - "Integrated payments", - "International payments'", - "Automated sales taxes", - "Accept local payments'", - "Recurring payments'" - ], - performanceFeatures: []), - - FullFeatureListGroups(title: "Marketing & Email", - essentialFeatures: [ - "Promote on TikTok", - "Sync with Pinterest", - "Connect with Facebook", - "Advanced SEO tools", - "Advertise on Google", - "Custom order emails", - ], - performanceFeatures: [ - "Back in stock emails", - "Marketing automation", - "Abandoned cart recovery", - "Referral programs", - "Customer birthday emails", - "Loyalty points programs" - ]), - - FullFeatureListGroups(title: "Shipping", - essentialFeatures: [ - "Shipment tracking", - "Live shipping rates", - "Discounted shipping²", - "Print shipping labels²"], - performanceFeatures: []), - ] - } -} - -struct FullFeatureListView: View { - @Environment(\.presentationMode) var presentationMode - - var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() - - var body: some View { - ScrollView() { - VStack(alignment: .leading, spacing: 8.0) { - ForEach(featureListGroups, id: \.title) { featureList in - Text(featureList.title) - .font(.title) - .bold() - .padding(.bottom) - ForEach(featureList.essentialFeatures, id: \.self) { feature in - Text(feature) - .font(.body) - } - ForEach(featureList.performanceFeatures, id: \.self) { feature in - HStack { - Text(feature) - .font(.body) - Image(systemName: "star.fill") - .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) - .font(.footnote) - } - } - Divider() - .padding(.bottom) - HStack { - Image(systemName: "star.fill") - Text("Performance plan only") - } - .font(.footnote) - .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) - .padding(.bottom) - .renderedIf(featureList.performanceFeatures.isNotEmpty) - } - Text(Localization.disclaimer1) - .font(.caption) - Text(Localization.disclaimer2) - .font(.caption) - } - .padding(.horizontal) - } - .padding() - .navigationTitle("Full Feature List") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(leading: Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(systemName: "chevron.left") - }) - } -} - -private extension FullFeatureListView { - struct Localization { - static let disclaimer1 = NSLocalizedString( - "1. Available as standard in WooCommerce Payments (restrictions apply)." + - "Additional extensions may be required for other payment providers." , - comment: "") - static let disclaimer2 = NSLocalizedString( - "2. Only available in the U.S. – an additional extension will be required for other countries.", - comment: "") - } -} - struct OwnerUpgradesView: View { @State var upgradePlans: [WooWPComPlan] @State var isPurchasing: Bool diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index aa96c0cca77..f6d40166995 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1257,6 +1257,7 @@ 68709D402A2EE2DC00A7FA6C /* UpgradesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68709D3F2A2EE2DC00A7FA6C /* UpgradesViewModel.swift */; }; 6879B8DB287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */; }; 6881CCC42A5EE6BF00AEDE36 /* WooPlanCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */; }; + 6888A2C82A668D650026F5C0 /* FullFeatureListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */; }; 68D1BEDB28FFEDC20074A29E /* OrderCustomerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D1BEDA28FFEDC20074A29E /* OrderCustomerListView.swift */; }; 68D1BEDD2900E4180074A29E /* CustomerSearchUICommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D1BEDC2900E4180074A29E /* CustomerSearchUICommand.swift */; }; 68E6749F2A4DA01C0034BA1E /* WooWPComPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */; }; @@ -3619,6 +3620,7 @@ 68709D3F2A2EE2DC00A7FA6C /* UpgradesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradesViewModel.swift; sourceTree = ""; }; 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsViewModelTests.swift; sourceTree = ""; }; 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPlanCardView.swift; sourceTree = ""; }; + 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullFeatureListView.swift; sourceTree = ""; }; 68D1BEDA28FFEDC20074A29E /* OrderCustomerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCustomerListView.swift; sourceTree = ""; }; 68D1BEDC2900E4180074A29E /* CustomerSearchUICommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSearchUICommand.swift; sourceTree = ""; }; 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooWPComPlan.swift; sourceTree = ""; }; @@ -6440,6 +6442,7 @@ 68E674AC2A4DAC010034BA1E /* CurrentPlanDetailsView.swift */, 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */, 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */, + 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */, ); name = Upgrades; path = Classes/ViewRelated/Upgrades; @@ -12517,6 +12520,7 @@ DE7B479527A38B8F0018742E /* CouponDetailsViewModel.swift in Sources */, E16058F9285876E600E471D4 /* LeftImageTitleSubtitleTableViewCell.swift in Sources */, 03E471C2293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift in Sources */, + 6888A2C82A668D650026F5C0 /* FullFeatureListView.swift in Sources */, DEC2962926C20ECB005A056B /* CollapsibleView.swift in Sources */, B9F3DAAF29BB73CD00DDD545 /* CreateOrderAppIntent.swift in Sources */, AEDDDA0A25CA9C980077F9B2 /* AttributePickerViewController.swift in Sources */, From bfc2d3e5c8c118929e5c32968cbc316eff0109b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 11:35:45 +0200 Subject: [PATCH 08/41] Remove unnecessary logic --- .../ViewRelated/Coupons/CouponListView.swift | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift index ec9c4334bc1..56f3a1c979b 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -3,36 +3,10 @@ import SwiftUI struct CouponListView: UIViewControllerRepresentable { let siteID: Int64 - typealias UIViewControllerType = CouponListViewController - - class Coordinator { - var parentObserver: NSKeyValueObservation? - var rightBarButtonItemsObserver: NSKeyValueObservation? - } - - /// This is a UIKit solution for fixing Navigation Title and Bar Button Items ignored in NavigationView. - /// This solution doesn't require making internal changes to the destination `UIViewController` - /// and should be called once, when wrapped. - /// Solution proposed here: https://stackoverflow.com/a/68567095/7241994 - /// func makeUIViewController(context: Self.Context) -> CouponListViewController { let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) - context.coordinator.parentObserver = viewController.observe(\.parent, changeHandler: { vc, _ in - vc.parent?.navigationItem.title = vc.title - vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems - }) - - // This fixes the issue when `rightBarButtonItem` is updated in `CouponListViewController`, - // the hosting controller should be updated to reflect the change. - context.coordinator.rightBarButtonItemsObserver = viewController.observe(\.navigationItem.rightBarButtonItems, changeHandler: { vc, _ in - vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems - }) return viewController } - func updateUIViewController(_ uiViewController: CouponListViewController, context: Context) { - // nothing to do here - } - - func makeCoordinator() -> Self.Coordinator { Coordinator() } + func updateUIViewController(_ uiViewController: CouponListViewController, context: Context) {} } From a0578f5b9c1e6030872e25ce445c49e7825da33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 11:36:41 +0200 Subject: [PATCH 09/41] Embed coupon list in a navigation view and add buttons --- .../PaymentSection/OrderPaymentSection.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 23deaa4fe45..707213f7855 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -71,7 +71,18 @@ struct OrderPaymentSection: View { addCouponRow .sheet(isPresented: $shouldShowAddCouponLineDetails) { - CouponListView(siteID: viewModel.siteID) + NavigationView { + CouponListView(siteID: viewModel.siteID) + .navigationTitle("Coupons") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + shouldShowAddCouponLineDetails = false + } + } + } + } } TitleAndValueRow(title: Localization.taxesTotal, value: .content(viewModel.taxesTotal)) From 9e1f2636ab41ccdc0fe197d3c0af11a71e7c6b9f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 11:59:57 +0200 Subject: [PATCH 10/41] Style disclaimers view --- .../Upgrades/FullFeatureListView.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index 09dd22a88ad..1a45c95ba49 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -73,9 +73,9 @@ struct FullFeatureListViewModel { essentialFeatures: [ "Shipment tracking", "Live shipping rates", - "Discounted shipping²", "Print shipping labels²"], - performanceFeatures: []), + performanceFeatures: [ + "Discounted shipping²"]), ] } } @@ -107,6 +107,7 @@ struct FullFeatureListView: View { } } Divider() + .padding(.top) .padding(.bottom) HStack { Image(systemName: "star.fill") @@ -117,15 +118,17 @@ struct FullFeatureListView: View { .padding(.bottom) .renderedIf(featureList.performanceFeatures.isNotEmpty) } - // Outside of the loop - VStack { - Text(Localization.disclaimer1) - .font(.caption) - Text(Localization.disclaimer2) - .font(.caption) - } } .padding(.horizontal) + .background(Color(.white)) + .cornerRadius(10.0) + VStack(alignment: .leading, spacing: 8.0) { + Text(Localization.disclaimer1) + .font(.caption) + Text(Localization.disclaimer2) + .font(.caption) + } + .background(Color(.secondarySystemBackground)) } .padding() .navigationTitle("Full Feature List") @@ -135,6 +138,7 @@ struct FullFeatureListView: View { }) { Image(systemName: "chevron.left") }) + .background(Color(.secondarySystemBackground)) } } From e9be09c600d1fdd679ddf36f2de2039a63475ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 12:26:17 +0200 Subject: [PATCH 11/41] Pass selected coupon to view model --- .../Classes/ViewRelated/Coupons/CouponListView.swift | 3 +++ .../Order Creation/EditableOrderViewModel.swift | 12 ++++++------ .../PaymentSection/OrderPaymentSection.swift | 5 ++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift index 56f3a1c979b..af6526bbf43 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -1,10 +1,13 @@ import SwiftUI +import Yosemite struct CouponListView: UIViewControllerRepresentable { let siteID: Int64 + let onCouponSelected: ((Coupon) -> Void) func makeUIViewController(context: Self.Context) -> CouponListViewController { let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) + viewController.onCouponSelected = onCouponSelected return viewController } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift index 9978f3d395f..844bd92abe1 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift @@ -698,7 +698,7 @@ extension EditableOrderViewModel { let shippingLineViewModel: ShippingLineDetailsViewModel let feeLineViewModel: FeeOrDiscountLineDetailsViewModel - let addCouponLineViewModel: CouponLineDetailsViewModel + let addNewCouponLineClosure: (Coupon) -> Void init(siteID: Int64 = 0, itemsTotal: String = "0", @@ -722,7 +722,7 @@ extension EditableOrderViewModel { showNonEditableIndicators: Bool = false, saveShippingLineClosure: @escaping (ShippingLine?) -> Void = { _ in }, saveFeeLineClosure: @escaping (String?) -> Void = { _ in }, - saveCouponLineClosure: @escaping (CouponLineDetailsResult) -> Void = { _ in }, + addNewCouponLineClosure: @escaping (Coupon) -> Void = { _ in }, currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) { self.siteID = siteID self.itemsTotal = currencyFormatter.formatAmount(itemsTotal) ?? "0.00" @@ -753,9 +753,7 @@ extension EditableOrderViewModel { initialTotal: feeLineTotal, lineType: .fee, didSelectSave: saveFeeLineClosure) - self.addCouponLineViewModel = CouponLineDetailsViewModel(isExistingCouponLine: false, - siteID: siteID, - didSelectSave: saveCouponLineClosure) + self.addNewCouponLineClosure = addNewCouponLineClosure } } @@ -1069,7 +1067,9 @@ private extension EditableOrderViewModel { showNonEditableIndicators: showNonEditableIndicators, saveShippingLineClosure: self.saveShippingLine, saveFeeLineClosure: self.saveFeeLine, - saveCouponLineClosure: self.saveCouponLine, + addNewCouponLineClosure: { [weak self] coupon in + self?.saveCouponLine(result: .added(newCode: coupon.code)) + }, currencyFormatter: self.currencyFormatter) } .assign(to: &$paymentDataViewModel) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 707213f7855..c32db63320f 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -72,7 +72,10 @@ struct OrderPaymentSection: View { addCouponRow .sheet(isPresented: $shouldShowAddCouponLineDetails) { NavigationView { - CouponListView(siteID: viewModel.siteID) + CouponListView(siteID: viewModel.siteID, onCouponSelected: { coupon in + viewModel.addNewCouponLineClosure(coupon) + shouldShowAddCouponLineDetails = false + }) .navigationTitle("Coupons") .navigationBarTitleDisplayMode(.inline) .toolbar { From 4429bd112bb302707a0661f56ef1c90ad94024f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 12:35:49 +0200 Subject: [PATCH 12/41] Remove unnecessary code --- .../Order Creation/EditableOrderViewModel.swift | 3 +-- .../PaymentSection/CouponLineDetails.swift | 11 +++-------- .../PaymentSection/CouponLineDetailsViewModel.swift | 11 ++--------- .../PaymentSection/OrderPaymentSection.swift | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift index 844bd92abe1..cb7802cc673 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift @@ -1341,8 +1341,7 @@ private extension EditableOrderViewModel { couponLines.map { CouponLineViewModel(title: String.localizedStringWithFormat(Localization.CouponSummary.singular, $0.code), discount: "-" + (currencyFormatter.formatAmount($0.discount) ?? "0.00"), - detailsViewModel: CouponLineDetailsViewModel(isExistingCouponLine: true, - code: $0.code, + detailsViewModel: CouponLineDetailsViewModel(code: $0.code, siteID: siteID, didSelectSave: saveCouponLine)) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetails.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetails.swift index 474a7fe5d7e..2456a6fdbd8 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetails.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetails.swift @@ -44,9 +44,7 @@ struct CouponLineDetails: View { .disabled(showValidateCouponLoading) Spacer(minLength: Layout.sectionSpacing) - - if viewModel.isExistingCouponLine { - Section { + Section { Button(Localization.remove) { focusedField = nil viewModel.removeCoupon() @@ -59,11 +57,10 @@ struct CouponLineDetails: View { } .background(Color(.listForeground(modal: false))) } - } } .background(Color(.listBackground)) .ignoresSafeArea(.container, edges: [.horizontal, .bottom]) - .navigationTitle(viewModel.isExistingCouponLine ? Localization.coupon : Localization.addCoupon) + .navigationTitle(Localization.coupon) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { @@ -102,7 +99,6 @@ private extension CouponLineDetails { } enum Localization { - static let addCoupon = NSLocalizedString("Add coupon", comment: "Title for the Coupon screen during order creation") static let coupon = NSLocalizedString("Coupon", comment: "Title for the Coupon screen during order creation") static let close = NSLocalizedString("Close", comment: "Text for the close button in the Coupon Details screen") static let done = NSLocalizedString("Done", comment: "Text for the done button in the Coupon Details screen") @@ -117,8 +113,7 @@ private extension CouponLineDetails { struct CouponLineDetails_Previews: PreviewProvider { static var previews: some View { - let viewModel = CouponLineDetailsViewModel(isExistingCouponLine: true, - code: "", + let viewModel = CouponLineDetailsViewModel(code: "", siteID: 0, didSelectSave: { _ in }) CouponLineDetails(viewModel: viewModel) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetailsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetailsViewModel.swift index 07aec2a3132..5823b00f1d9 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/CouponLineDetailsViewModel.swift @@ -21,10 +21,6 @@ final class CouponLineDetailsViewModel: Identifiable, ObservableObject { /// @Published var code: String = "" - /// Returns true when existing coupon line is edited. - /// - let isExistingCouponLine: Bool - /// Returns true when there are no valid pending changes. /// var shouldDisableDoneButton: Bool { @@ -43,12 +39,10 @@ final class CouponLineDetailsViewModel: Identifiable, ObservableObject { private let stores: StoresManager - init(isExistingCouponLine: Bool, - code: String? = nil, + init(code: String? = nil, siteID: Int64, stores: StoresManager = ServiceLocator.stores, didSelectSave: @escaping ((CouponLineDetailsResult) -> Void)) { - self.isExistingCouponLine = isExistingCouponLine self.code = code ?? "" self.siteID = siteID self.stores = stores @@ -87,8 +81,7 @@ final class CouponLineDetailsViewModel: Identifiable, ObservableObject { private extension CouponLineDetailsViewModel { func saveData() { - guard isExistingCouponLine, - let initialCode = initialCode, + guard let initialCode = initialCode, initialCode.isNotEmpty else { return didSelectSave(.added(newCode: code)) } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index c32db63320f..fca38002c06 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -154,8 +154,8 @@ private extension OrderPaymentSection { static let addFee = NSLocalizedString("Add Fee", comment: "Title text of the button that adds a fee when creating a new order") static let feesTotal = NSLocalizedString("Fees", comment: "Label for the row showing the cost of fees in the order") static let taxesTotal = NSLocalizedString("Taxes", comment: "Label for the row showing the taxes in the order") + static let addCoupon = NSLocalizedString("Add coupon", comment: "Title for the Coupon screen during order creation") static let coupon = NSLocalizedString("Coupon", comment: "Label for the row showing the cost of coupon in the order") - static let addCoupon = NSLocalizedString("Add Coupon", comment: "Title text of the button that adds a coupon when creating a new order") } } From 32bd22da4430e3464b628d938c09980ccb69d1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 12:38:52 +0200 Subject: [PATCH 13/41] Add localized strings --- .../Order Creation/PaymentSection/OrderPaymentSection.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index fca38002c06..89762730fe8 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -76,11 +76,11 @@ struct OrderPaymentSection: View { viewModel.addNewCouponLineClosure(coupon) shouldShowAddCouponLineDetails = false }) - .navigationTitle("Coupons") + .navigationTitle(Localization.addCoupon) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { + Button(Localization.cancelButton) { shouldShowAddCouponLineDetails = false } } @@ -156,6 +156,7 @@ private extension OrderPaymentSection { static let taxesTotal = NSLocalizedString("Taxes", comment: "Label for the row showing the taxes in the order") static let addCoupon = NSLocalizedString("Add coupon", comment: "Title for the Coupon screen during order creation") static let coupon = NSLocalizedString("Coupon", comment: "Label for the row showing the cost of coupon in the order") + static let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title when showing the coupon list selector") } } From 62fb7cab5d99ce83969d3917e716fbd0c43d1eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 12:39:53 +0200 Subject: [PATCH 14/41] Remove unnecessary import --- .../Order Creation/PaymentSection/OrderPaymentSection.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 89762730fe8..11fa99db2f0 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -1,5 +1,4 @@ import SwiftUI -import Yosemite /// Represents the Payment section in an order /// From 9c2c1f4189085c20f9a50425eb96053a4e69049d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 13:38:22 +0200 Subject: [PATCH 15/41] Update tests to new changes --- .../CouponLineDetailsViewModelTests.swift | 57 +------------------ 1 file changed, 1 insertion(+), 56 deletions(-) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/CouponLineDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/CouponLineDetailsViewModelTests.swift index 4d214f75280..41176788b32 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/CouponLineDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/CouponLineDetailsViewModelTests.swift @@ -13,8 +13,7 @@ final class CouponLineDetailsViewModelTests: XCTestCase { override func setUp() { super.setUp() stores = MockStoresManager(sessionManager: SessionManager.makeForTesting()) - viewModel = CouponLineDetailsViewModel(isExistingCouponLine: true, - code: initialCode, + viewModel = CouponLineDetailsViewModel(code: initialCode, siteID: sampleSiteID, stores: stores, didSelectSave: { _ in }) @@ -60,17 +59,6 @@ final class CouponLineDetailsViewModelTests: XCTestCase { XCTAssertTrue(viewModel.shouldDisableDoneButton) } - func test_view_model_initializes_correctly_with_no_existing_coupon_line() { - // Given - let viewModel = CouponLineDetailsViewModel(isExistingCouponLine: false, - code: "", - siteID: sampleSiteID, - didSelectSave: { _ in }) - - // Then - XCTAssertFalse(viewModel.isExistingCouponLine) - } - func test_validateAndSaveData_then_calls_action_with_right_parameters() { // Given let passedCouponCode = "COUPON_CODE" @@ -137,49 +125,6 @@ final class CouponLineDetailsViewModelTests: XCTestCase { } } - func test_validateAndSaveData_when_coupon_is_new_and_validated_then_completes_successfully() { - // Given - var savedResult: CouponLineDetailsResult? - viewModel = CouponLineDetailsViewModel(isExistingCouponLine: false, - code: "", - siteID: sampleSiteID, - stores: stores, - didSelectSave: { _ in }) - viewModel.didSelectSave = { result in - savedResult = result - } - - let passedCouponCode = "COUPON" - viewModel.code = passedCouponCode - - - stores.whenReceivingAction(ofType: CouponAction.self) { action in - switch action { - case let .validateCouponCode(_, _, onCompletion): - onCompletion(.success(true)) - default: - break - } - } - - // When - let shouldDismiss = waitFor { [weak self] promise in - self?.viewModel.validateAndSaveData() { shouldDismiss in - promise(shouldDismiss) - } - } - - // Then - XCTAssertTrue(shouldDismiss) - - switch savedResult { - case let .added(newCode): - XCTAssertEqual(newCode, passedCouponCode) - default: - XCTFail("Result should be added case") - } - } - func test_validateAndSaveData_when_coupon_is_not_validated_then_fails() { // Given stores.whenReceivingAction(ofType: CouponAction.self) { action in From a25015d54b377a31ab57a45efaaf779ddc7d59d3 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Tue, 18 Jul 2023 17:28:25 +0530 Subject: [PATCH 16/41] Add photo -> product AB test case. --- Experiments/Experiments/ABTest.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Experiments/Experiments/ABTest.swift b/Experiments/Experiments/ABTest.swift index 066f8482c93..5ce9021263e 100644 --- a/Experiments/Experiments/ABTest.swift +++ b/Experiments/Experiments/ABTest.swift @@ -10,6 +10,10 @@ public enum ABTest: String, Codable, CaseIterable { /// Experiment ref: pbxNRc-1QS-p2 case aaTestLoggedIn = "woocommerceios_explat_aa_test_logged_in_202212_v2" + /// A/B test for flow to add product from an image. + /// Experiment ref: pbxNRc-2MS-p2 + case addProductFromImage = "woocommerceios_add_product_from_photo_202307" + /// A/A test to make sure there is no bias in the logged out state. /// Experiment ref: pbxNRc-1S0-p2 case aaTestLoggedOut = "woocommerceios_explat_aa_test_logged_out_202212_v2" @@ -27,6 +31,8 @@ public enum ABTest: String, Codable, CaseIterable { switch self { case .aaTestLoggedIn: return .loggedIn + case .addProductFromImage: + return .loggedIn case .aaTestLoggedOut: return .loggedOut // Mocks From 97838fb741c5ed73222adaffb5c171cb28b458e3 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Tue, 18 Jul 2023 17:28:59 +0530 Subject: [PATCH 17/41] Mark eligible only if treatment. --- .../AddProductFromImageEligibilityChecker.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift index 6d56c89393b..c9e2dfb61d2 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift @@ -1,6 +1,7 @@ import Foundation import Yosemite import protocol Experiments.FeatureFlagService +import Experiments /// Protocol for checking "add product from image" eligibility for easier unit testing. protocol AddProductFromImageEligibilityCheckerProtocol { @@ -34,7 +35,10 @@ final class AddProductFromImageEligibilityChecker: AddProductFromImageEligibilit return false } - // TODO: 10180 - A/B experiment check - return featureFlagService.isFeatureFlagEnabled(.addProductFromImage) + guard featureFlagService.isFeatureFlagEnabled(.addProductFromImage) else { + return false + } + + return ABTest.addProductFromImage.variation == .treatment } } From 7578a089a8613813874e513a3a01d8a541bd5d71 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 14:06:43 +0200 Subject: [PATCH 18/41] Extract title strings to localization struct --- .../Upgrades/FullFeatureListView.swift | 72 +++++++++++++++---- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index 1a45c95ba49..774f63aedf2 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -10,7 +10,7 @@ struct FullFeatureListGroups { struct FullFeatureListViewModel { static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { return [ - FullFeatureListGroups(title: "Your Store", + FullFeatureListGroups(title: Localization.yourStoreFeatureTitle, essentialFeatures: [ "WooCommerce store", "WooCommerce mobile app", @@ -29,19 +29,19 @@ struct FullFeatureListViewModel { ], performanceFeatures: [] ), - FullFeatureListGroups(title: "Products", + FullFeatureListGroups(title: Localization.productsFeatureTitle, essentialFeatures: [ "List unlimited products", "Gift cards", + "List products by brand", ], performanceFeatures: [ "Min/Max order quantity", "Product Bundles", "Custom product kits", - "List products by brand", "Product recommendations" ]), - FullFeatureListGroups(title: "Payments", + FullFeatureListGroups(title: Localization.paymentsFeatureTitle, essentialFeatures: [ "Integrated payments", "International payments'", @@ -51,7 +51,7 @@ struct FullFeatureListViewModel { ], performanceFeatures: []), - FullFeatureListGroups(title: "Marketing & Email", + FullFeatureListGroups(title: Localization.marketingAndEmailFeatureTitle, essentialFeatures: [ "Promote on TikTok", "Sync with Pinterest", @@ -69,7 +69,7 @@ struct FullFeatureListViewModel { "Loyalty points programs" ]), - FullFeatureListGroups(title: "Shipping", + FullFeatureListGroups(title: Localization.shippingFeatureTitle, essentialFeatures: [ "Shipment tracking", "Live shipping rates", @@ -80,6 +80,34 @@ struct FullFeatureListViewModel { } } +private extension FullFeatureListViewModel { + struct Localization { + static let yourStoreFeatureTitle = NSLocalizedString( + "Your Store", + comment: "The title of one of the feature groups offered with paid plans") + + static let productsFeatureTitle = NSLocalizedString( + "Products", + comment: "The title of one of the feature groups offered with paid plans") + + static let paymentsFeatureTitle = NSLocalizedString( + "Payments", + comment: "The title of one of the feature groups offered with paid plans") + + static let marketingAndEmailFeatureTitle = NSLocalizedString( + "Marketing & Email", + comment: "The title of one of the feature groups offered with paid plans") + + static let shippingFeatureTitle = NSLocalizedString( + "Shipping", + comment: "The title of one of the feature groups offered with paid plans") + + static let WooCommerceStore = NSLocalizedString( + "WooCommerce store", + comment: "The title of one of the features offered with the Essential plan") + } +} + struct FullFeatureListView: View { @Environment(\.presentationMode) var presentationMode @@ -87,7 +115,7 @@ struct FullFeatureListView: View { var body: some View { ScrollView() { - VStack(alignment: .leading, spacing: 8.0) { + VStack(alignment: .leading, spacing: FullFeatureListView.Layout.featureListSpacing) { ForEach(featureListGroups, id: \.title) { featureList in Text(featureList.title) .font(.title) @@ -111,7 +139,7 @@ struct FullFeatureListView: View { .padding(.bottom) HStack { Image(systemName: "star.fill") - Text("Performance plan only") + Text(Localization.performanceOnlyText) } .font(.footnote) .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) @@ -121,17 +149,17 @@ struct FullFeatureListView: View { } .padding(.horizontal) .background(Color(.white)) - .cornerRadius(10.0) - VStack(alignment: .leading, spacing: 8.0) { - Text(Localization.disclaimer1) + .cornerRadius(Layout.featureListCornerRadius) + VStack(alignment: .leading, spacing: Layout.featureListSpacing) { + Text(Localization.paymentsDisclaimerText) .font(.caption) - Text(Localization.disclaimer2) + Text(Localization.pluginsDisclaimerText) .font(.caption) } .background(Color(.secondarySystemBackground)) } .padding() - .navigationTitle("Full Feature List") + .navigationTitle(Localization.featureListTitleText) .navigationBarTitleDisplayMode(.inline) .navigationBarItems(leading: Button(action: { presentationMode.wrappedValue.dismiss() @@ -144,12 +172,26 @@ struct FullFeatureListView: View { private extension FullFeatureListView { struct Localization { - static let disclaimer1 = NSLocalizedString( + static let featureListTitleText = NSLocalizedString( + "Full Feature List", + comment: "") + + static let performanceOnlyText = NSLocalizedString( + "Performance plan only", + comment: "") + + static let paymentsDisclaimerText = NSLocalizedString( "1. Available as standard in WooCommerce Payments (restrictions apply)." + "Additional extensions may be required for other payment providers." , comment: "") - static let disclaimer2 = NSLocalizedString( + + static let pluginsDisclaimerText = NSLocalizedString( "2. Only available in the U.S. – an additional extension will be required for other countries.", comment: "") } + + struct Layout { + static let featureListSpacing: CGFloat = 8.0 + static let featureListCornerRadius: CGFloat = 10.0 + } } From 8ca9662d509a0616f2115fcddf5d600501439741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 14:31:32 +0200 Subject: [PATCH 19/41] Send user to menu if they don't have any coupons --- .../ViewRelated/Coupons/CouponListView.swift | 3 +++ .../Coupons/CouponListViewController.swift | 24 ++++++++++++++++--- .../EnhancedCouponListViewController.swift | 24 +++---------------- .../ViewRelated/MainTabBarController.swift | 5 ++-- .../PaymentSection/OrderPaymentSection.swift | 12 ++++++---- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift index af6526bbf43..fa9d418698b 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -3,11 +3,14 @@ import Yosemite struct CouponListView: UIViewControllerRepresentable { let siteID: Int64 + let emptyStateCreateCouponAction: (() -> Void) let onCouponSelected: ((Coupon) -> Void) + func makeUIViewController(context: Self.Context) -> CouponListViewController { let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) viewController.onCouponSelected = onCouponSelected + viewController.emptyStateCreateCouponAction = emptyStateCreateCouponAction return viewController } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift index b5028cac47e..7e5eda6a098 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift @@ -39,7 +39,7 @@ final class CouponListViewController: UIViewController, GhostableViewController private lazy var topBannerView: TopBannerView = createFeedbackBannerView() var onDataLoaded: ((Bool) -> Void)? - var noResultConfig: EmptyStateViewController.Config? + var emptyStateCreateCouponAction: (() -> Void)? var onCouponSelected: ((Coupon) -> Void)? init(siteID: Int64, showFeedbackBannerIfAppropriate: Bool) { @@ -288,8 +288,17 @@ private extension CouponListViewController { let emptyStateViewController = EmptyStateViewController(style: .list) displayEmptyStateViewController(emptyStateViewController) - if let noResultConfig = noResultConfig { - emptyStateViewController.configure(noResultConfig) + if let emptyStateCreateCouponAction = emptyStateCreateCouponAction { + let configuration = EmptyStateViewController.Config.withButton( + message: .init(string: Localization.couponCreationSuggestionMessage), + image: .emptyCouponsImage, + details: Localization.emptyStateDetails, + buttonTitle: Localization.createCouponAction + ) { _ in + emptyStateCreateCouponAction() + } + + emptyStateViewController.configure(configuration) } } @@ -383,5 +392,14 @@ private extension CouponListViewController { static let giveFeedbackAction = NSLocalizedString("Give feedback", comment: "Title of the feedback action button on the coupon list screen") static let dismissAction = NSLocalizedString("Dismiss", comment: "Title of the dismiss action button on the coupon list screen") + + static let couponCreationSuggestionMessage = NSLocalizedString( + "Everyone loves a deal", + comment: "The title on the placeholder overlay when there are no coupons on the coupon list screen and creating a coupon is possible.") + static let emptyStateDetails = NSLocalizedString( + "Boost your business by sending customers special offers and discounts.", + comment: "The details text on the placeholder overlay when there are no coupons on the coupon list screen.") + static let createCouponAction = NSLocalizedString("Create Coupon", + comment: "Title of the create coupon button on the coupon list screen when it's empty") } } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift index 67dad7d2e96..68dbe5d915b 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift @@ -22,7 +22,7 @@ final class EnhancedCouponListViewController: UIViewController { super.init(nibName: nil, bundle: nil) couponListViewController.onDataLoaded = configureNavigationBarItems - couponListViewController.noResultConfig = buildNoResultConfig() + couponListViewController.emptyStateCreateCouponAction = displayCouponTypeBottomSheet couponListViewController.onCouponSelected = showDetails } @@ -89,18 +89,6 @@ private extension EnhancedCouponListViewController { } } - func buildNoResultConfig() -> EmptyStateViewController.Config { - return .withButton( - message: .init(string: Localization.couponCreationSuggestionMessage), - image: .emptyCouponsImage, - details: Localization.emptyStateDetails, - buttonTitle: Localization.createCouponAction - ) { [weak self] button in - guard let self = self else { return } - self.displayCouponTypeBottomSheet() - } - } - /// Shows `SearchViewController`. /// @objc private func displaySearchCoupons() { @@ -169,19 +157,13 @@ private extension EnhancedCouponListViewController { static let accessibilityLabelCreateCoupons = NSLocalizedString("Create coupons", comment: "Accessibility label for the Create Coupons button") static let accessibilityHintCreateCoupons = NSLocalizedString("Start a Coupon creation by selecting a discount type in a bottom sheet", comment: "VoiceOver accessibility hint, informing the user the button can be used to create coupons.") - static let createCouponAction = NSLocalizedString("Create Coupon", - comment: "Title of the create coupon button on the coupon list screen when it's empty") static let accessibilityLabelSearchCoupons = NSLocalizedString("Search coupons", comment: "Accessibility label for the Search Coupons button") static let accessibilityHintSearchCoupons = NSLocalizedString( "Retrieves a list of coupons that contain a given keyword.", comment: "VoiceOver accessibility hint, informing the user the button can be used to search coupons." ) - static let couponCreationSuggestionMessage = NSLocalizedString( - "Everyone loves a deal", - comment: "The title on the placeholder overlay when there are no coupons on the coupon list screen and creating a coupon is possible.") - static let emptyStateDetails = NSLocalizedString( - "Boost your business by sending customers special offers and discounts.", - comment: "The details text on the placeholder overlay when there are no coupons on the coupon list screen.") static let couponDeleted = NSLocalizedString("Coupon deleted", comment: "Notice message after deleting coupon from the Coupon Details screen") + static let createCouponAction = NSLocalizedString("Create Coupon", + comment: "Title of the create coupon button on the coupon list screen when it's empty") } } diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 10e33898c3b..97580a55de2 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -195,9 +195,8 @@ final class MainTabBarController: UITabBarController { /// - animated: Whether the tab switch is animated. /// - completion: Invoked when switching to the tab's root screen is complete. func navigateToTabWithNavigationController(_ tab: WooTab, animated: Bool = false, completion: ((UINavigationController) -> Void)? = nil) { - if let presentedController = Self.childViewController()?.presentedViewController { - presentedController.dismiss(animated: true) - } + view.window?.rootViewController?.dismiss(animated: true, completion: nil) + selectedIndex = tab.visibleIndex() if let navController = selectedViewController as? UINavigationController { navController.popToRootViewController(animated: animated) { diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 11fa99db2f0..c6eb0d4a6ca 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -71,10 +71,14 @@ struct OrderPaymentSection: View { addCouponRow .sheet(isPresented: $shouldShowAddCouponLineDetails) { NavigationView { - CouponListView(siteID: viewModel.siteID, onCouponSelected: { coupon in - viewModel.addNewCouponLineClosure(coupon) - shouldShowAddCouponLineDetails = false - }) + CouponListView(siteID: viewModel.siteID, + emptyStateCreateCouponAction: { + MainTabBarController.switchToHubMenuTab() + }, + onCouponSelected: { coupon in + viewModel.addNewCouponLineClosure(coupon) + shouldShowAddCouponLineDetails = false + }) .navigationTitle(Localization.addCoupon) .navigationBarTitleDisplayMode(.inline) .toolbar { From d1353033eb223c27be7b4a8528eab5567daabcfb Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 14:50:53 +0200 Subject: [PATCH 20/41] Make essential feature strings NSLocalized --- .../Upgrades/FullFeatureListView.swift | 190 ++++++++++++++---- 1 file changed, 155 insertions(+), 35 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index 774f63aedf2..c7c768659a3 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -12,28 +12,28 @@ struct FullFeatureListViewModel { return [ FullFeatureListGroups(title: Localization.yourStoreFeatureTitle, essentialFeatures: [ - "WooCommerce store", - "WooCommerce mobile app", - "WordPress CMS", - "WordPress mobile app", - "Free SSL certificate", - "Generous storage", - "Automated backup + quick restore", - "Ad-free experience", - "Unlimited admin accounts", - "Live chat support", - "Email support", - "Premium themes included", - "Sales reports", - "Google Analytics" + Localization.wooCommerceStoreText, + Localization.wooCommerceMobileAppText, + Localization.wordPressCMSText, + Localization.wordPressMobileAppText, + Localization.freeSSLCertificateText, + Localization.generousStorageText, + Localization.automatedBackupQuickRestoreText, + Localization.adFreeExperienceText, + Localization.unlimitedAdminAccountsText, + Localization.liveChatSupportText, + Localization.emailSupportText, + Localization.premiumThemesIncludedText, + Localization.salesReportsText, + Localization.googleAnalyticsText, ], performanceFeatures: [] ), FullFeatureListGroups(title: Localization.productsFeatureTitle, essentialFeatures: [ - "List unlimited products", - "Gift cards", - "List products by brand", + Localization.listUnlimitedProducts, + Localization.giftCards, + Localization.listProductsByBrand, ], performanceFeatures: [ "Min/Max order quantity", @@ -43,22 +43,22 @@ struct FullFeatureListViewModel { ]), FullFeatureListGroups(title: Localization.paymentsFeatureTitle, essentialFeatures: [ - "Integrated payments", - "International payments'", - "Automated sales taxes", - "Accept local payments'", - "Recurring payments'" + Localization.integratedPayments, + Localization.internationalPayments, + Localization.automatedSalesTaxes, + Localization.acceptLocalPayments, + Localization.recurringPayments, ], performanceFeatures: []), FullFeatureListGroups(title: Localization.marketingAndEmailFeatureTitle, essentialFeatures: [ - "Promote on TikTok", - "Sync with Pinterest", - "Connect with Facebook", - "Advanced SEO tools", - "Advertise on Google", - "Custom order emails", + Localization.promoteOnTikTok, + Localization.syncWithPinterest, + Localization.connectWithFacebook, + Localization.advancedSeoTools, + Localization.advertiseOnGoogle, + Localization.customOrderEmails, ], performanceFeatures: [ "Back in stock emails", @@ -71,9 +71,10 @@ struct FullFeatureListViewModel { FullFeatureListGroups(title: Localization.shippingFeatureTitle, essentialFeatures: [ - "Shipment tracking", - "Live shipping rates", - "Print shipping labels²"], + Localization.shipmentTracking, + Localization.liveShippingRates, + Localization.printShippingLabels + ], performanceFeatures: [ "Discounted shipping²"]), ] @@ -85,11 +86,11 @@ private extension FullFeatureListViewModel { static let yourStoreFeatureTitle = NSLocalizedString( "Your Store", comment: "The title of one of the feature groups offered with paid plans") - + static let productsFeatureTitle = NSLocalizedString( "Products", comment: "The title of one of the feature groups offered with paid plans") - + static let paymentsFeatureTitle = NSLocalizedString( "Payments", comment: "The title of one of the feature groups offered with paid plans") @@ -101,10 +102,129 @@ private extension FullFeatureListViewModel { static let shippingFeatureTitle = NSLocalizedString( "Shipping", comment: "The title of one of the feature groups offered with paid plans") - - static let WooCommerceStore = NSLocalizedString( + + static let wooCommerceStoreText = NSLocalizedString( "WooCommerce store", comment: "The title of one of the features offered with the Essential plan") + + static let wooCommerceMobileAppText = NSLocalizedString( + "WooCommerce mobile app", + comment: "The title of one of the features offered with the Essential plan") + + static let wordPressCMSText = NSLocalizedString( + "WordPress CMS", + comment: "The title of one of the features offered with the Essential plan") + + static let wordPressMobileAppText = NSLocalizedString( + "WordPress mobile app", + comment: "The title of one of the features offered with the Essential plan") + + static let freeSSLCertificateText = NSLocalizedString( + "Free SSL certificate", + comment: "The title of one of the features offered with the Essential plan") + + static let generousStorageText = NSLocalizedString( + "Generous storage", + comment: "The title of one of the features offered with the Essential plan") + + static let automatedBackupQuickRestoreText = NSLocalizedString( + "Automated backup + quick restore", + comment: "The title of one of the features offered with the Essential plan") + + static let adFreeExperienceText = NSLocalizedString( + "Ad-free experience", + comment: "The title of one of the features offered with the Essential plan") + + static let unlimitedAdminAccountsText = NSLocalizedString( + "Unlimited admin accounts", + comment: "The title of one of the features offered with the Essential plan") + + static let liveChatSupportText = NSLocalizedString( + "Live chat support", + comment: "The title of one of the features offered with the Essential plan") + + static let emailSupportText = NSLocalizedString( + "Email support", + comment: "The title of one of the features offered with the Essential plan") + + static let premiumThemesIncludedText = NSLocalizedString( + "Premium themes included", + comment: "The title of one of the features offered with the Essential plan") + + static let salesReportsText = NSLocalizedString( + "Sales reports", + comment: "The title of one of the features offered with the Essential plan") + + static let googleAnalyticsText = NSLocalizedString( + "Google Analytics", + comment: "The title of one of the features offered with the Essential plan") + + static let listUnlimitedProducts = NSLocalizedString( + "List unlimited products", + comment: "The title of one of the features offered with the Essential plan") + + static let giftCards = NSLocalizedString( + "Gift cards", + comment: "The title of one of the features offered with the Essential plan") + + static let listProductsByBrand = NSLocalizedString( + "List products by brand", + comment: "The title of one of the features offered with the Essential plan") + + static let integratedPayments = NSLocalizedString( + "Integrated payments", + comment: "The title of one of the features offered with the Essential plan") + + static let internationalPayments = NSLocalizedString( + "International payments'", + comment: "The title of one of the features offered with the Essential plan") + + static let automatedSalesTaxes = NSLocalizedString( + "Automated sales taxes", + comment: "The title of one of the features offered with the Essential plan") + + static let acceptLocalPayments = NSLocalizedString( + "Accept local payments'", + comment: "The title of one of the features offered with the Essential plan") + + static let recurringPayments = NSLocalizedString( + "Recurring payments'", + comment: "The title of one of the features offered with the Essential plan") + static let promoteOnTikTok = NSLocalizedString( + "Promote on TikTok", + comment: "The title of one of the features offered with the Essential plan") + + static let syncWithPinterest = NSLocalizedString( + "Sync with Pinterest", + comment: "The title of one of the features offered with the Essential plan") + + static let connectWithFacebook = NSLocalizedString( + "Connect with Facebook", + comment: "The title of one of the features offered with the Essential plan") + + static let advancedSeoTools = NSLocalizedString( + "Advanced SEO tools", + comment: "The title of one of the features offered with the Essential plan") + + static let advertiseOnGoogle = NSLocalizedString( + "Advertise on Google", + comment: "The title of one of the features offered with the Essential plan") + + static let customOrderEmails = NSLocalizedString( + "Custom order emails", + comment: "The title of one of the features offered with the Essential plan") + + static let shipmentTracking = NSLocalizedString( + "Shipment tracking", + comment: "The title of one of the features offered with the Essential plan") + + static let liveShippingRates = NSLocalizedString( + "Live shipping rates", + comment: "The title of one of the features offered with the Essential plan") + + static let printShippingLabels = NSLocalizedString( + "Print shipping labels²", + comment: "The title of one of the features offered with the Essential plan") } } From 7f39b5f7c482aebd4860f892fa7cdac221b28108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 14:52:17 +0200 Subject: [PATCH 21/41] Navigate user to the coupons section when there's no coupons added. --- .../Classes/ViewRelated/Coupons/CouponListView.swift | 6 ++++-- .../ViewRelated/Coupons/CouponListViewController.swift | 10 +++++----- .../Coupons/EnhancedCouponListViewController.swift | 3 ++- .../ViewRelated/Hub Menu/HubMenuViewController.swift | 5 +++++ .../Classes/ViewRelated/MainTabBarController.swift | 10 ++++++++++ .../PaymentSection/OrderPaymentSection.swift | 7 +++++-- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift index fa9d418698b..cfc39078c27 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -3,14 +3,16 @@ import Yosemite struct CouponListView: UIViewControllerRepresentable { let siteID: Int64 - let emptyStateCreateCouponAction: (() -> Void) + let emptyStateActionTitle: String + let emptyStateAction: (() -> Void) let onCouponSelected: ((Coupon) -> Void) func makeUIViewController(context: Self.Context) -> CouponListViewController { let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) + viewController.emptyStateActionTitle = emptyStateActionTitle viewController.onCouponSelected = onCouponSelected - viewController.emptyStateCreateCouponAction = emptyStateCreateCouponAction + viewController.emptyStateAction = emptyStateAction return viewController } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift index 7e5eda6a098..7cfd11dc267 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift @@ -39,7 +39,8 @@ final class CouponListViewController: UIViewController, GhostableViewController private lazy var topBannerView: TopBannerView = createFeedbackBannerView() var onDataLoaded: ((Bool) -> Void)? - var emptyStateCreateCouponAction: (() -> Void)? + var emptyStateAction: (() -> Void)? + var emptyStateActionTitle: String? var onCouponSelected: ((Coupon) -> Void)? init(siteID: Int64, showFeedbackBannerIfAppropriate: Bool) { @@ -288,12 +289,13 @@ private extension CouponListViewController { let emptyStateViewController = EmptyStateViewController(style: .list) displayEmptyStateViewController(emptyStateViewController) - if let emptyStateCreateCouponAction = emptyStateCreateCouponAction { + if let emptyStateCreateCouponAction = emptyStateAction, + let emptyStateActionTitle = emptyStateActionTitle { let configuration = EmptyStateViewController.Config.withButton( message: .init(string: Localization.couponCreationSuggestionMessage), image: .emptyCouponsImage, details: Localization.emptyStateDetails, - buttonTitle: Localization.createCouponAction + buttonTitle: emptyStateActionTitle ) { _ in emptyStateCreateCouponAction() } @@ -399,7 +401,5 @@ private extension CouponListViewController { static let emptyStateDetails = NSLocalizedString( "Boost your business by sending customers special offers and discounts.", comment: "The details text on the placeholder overlay when there are no coupons on the coupon list screen.") - static let createCouponAction = NSLocalizedString("Create Coupon", - comment: "Title of the create coupon button on the coupon list screen when it's empty") } } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift index 68dbe5d915b..8ace10fd543 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift @@ -22,7 +22,8 @@ final class EnhancedCouponListViewController: UIViewController { super.init(nibName: nil, bundle: nil) couponListViewController.onDataLoaded = configureNavigationBarItems - couponListViewController.emptyStateCreateCouponAction = displayCouponTypeBottomSheet + couponListViewController.emptyStateAction = displayCouponTypeBottomSheet + couponListViewController.emptyStateActionTitle = Localization.createCouponAction couponListViewController.onCouponSelected = showDetails } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift index 38f9ef48587..5467df68131 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift @@ -43,6 +43,11 @@ final class HubMenuViewController: UIHostingController { return inPersonPaymentsMenuViewController } + func showCoupons() { + let enhancedCouponListViewController = EnhancedCouponListViewController(siteID: viewModel.siteID) + show(enhancedCouponListViewController, sender: self) + } + /// Pushes the Settings & Privacy screen onto the navigation stack. /// func showPrivacySettings() { diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 97580a55de2..b213641dcfb 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -452,6 +452,16 @@ extension MainTabBarController { return hubMenuViewController.showPaymentsMenu() } + static func presentCoupons() { + switchToHubMenuTab() + + guard let hubMenuViewController: HubMenuViewController = childViewController() else { + return + } + + hubMenuViewController.showCoupons() + } + static func presentCollectPayment() { let viewController = presentPayments() diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index c6eb0d4a6ca..ef90966d58b 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -72,8 +72,9 @@ struct OrderPaymentSection: View { .sheet(isPresented: $shouldShowAddCouponLineDetails) { NavigationView { CouponListView(siteID: viewModel.siteID, - emptyStateCreateCouponAction: { - MainTabBarController.switchToHubMenuTab() + emptyStateActionTitle: Localization.goToCoupons, + emptyStateAction: { + MainTabBarController.presentCoupons() }, onCouponSelected: { coupon in viewModel.addNewCouponLineClosure(coupon) @@ -159,6 +160,8 @@ private extension OrderPaymentSection { static let taxesTotal = NSLocalizedString("Taxes", comment: "Label for the row showing the taxes in the order") static let addCoupon = NSLocalizedString("Add coupon", comment: "Title for the Coupon screen during order creation") static let coupon = NSLocalizedString("Coupon", comment: "Label for the row showing the cost of coupon in the order") + static let goToCoupons = NSLocalizedString("Go to Coupons", comment: "Button title on the Coupon screen empty state" + + "when creating a new order that navigates to the Coupons Section") static let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title when showing the coupon list selector") } } From d9a6ef3b29dc9f8e13ace452f1c3c3b57d149b4b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 14:58:50 +0200 Subject: [PATCH 22/41] Make performance feature strings NSLocalized --- .../Upgrades/FullFeatureListView.swift | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index c7c768659a3..ab01e18a9ef 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -36,10 +36,10 @@ struct FullFeatureListViewModel { Localization.listProductsByBrand, ], performanceFeatures: [ - "Min/Max order quantity", - "Product Bundles", - "Custom product kits", - "Product recommendations" + Localization.minMaxOrderQuantityText, + Localization.productBundlesText, + Localization.customProductKitsText, + Localization.productRecommendationsText, ]), FullFeatureListGroups(title: Localization.paymentsFeatureTitle, essentialFeatures: [ @@ -61,12 +61,12 @@ struct FullFeatureListViewModel { Localization.customOrderEmails, ], performanceFeatures: [ - "Back in stock emails", - "Marketing automation", - "Abandoned cart recovery", - "Referral programs", - "Customer birthday emails", - "Loyalty points programs" + Localization.backInStockEmailsText, + Localization.marketingAutomationText, + Localization.abandonedCartRecoveryText, + Localization.referralProgramsText, + Localization.customerBirthdayEmailsText, + Localization.loyaltyPointsProgramsText, ]), FullFeatureListGroups(title: Localization.shippingFeatureTitle, @@ -76,7 +76,8 @@ struct FullFeatureListViewModel { Localization.printShippingLabels ], performanceFeatures: [ - "Discounted shipping²"]), + Localization.discountedShippingText, + ]), ] } } @@ -225,6 +226,50 @@ private extension FullFeatureListViewModel { static let printShippingLabels = NSLocalizedString( "Print shipping labels²", comment: "The title of one of the features offered with the Essential plan") + + static let minMaxOrderQuantityText = NSLocalizedString( + "Min/Max order quantity", + comment: "The title of one of the features offered with the Performance plan") + + static let productBundlesText = NSLocalizedString( + "Product Bundles", + comment: "The title of one of the features offered with the Performance plan") + + static let customProductKitsText = NSLocalizedString( + "Custom product kits", + comment: "The title of one of the features offered with the Performance plan") + + static let productRecommendationsText = NSLocalizedString( + "Product recommendations", + comment: "The title of one of the features offered with the Performance plan") + + static let backInStockEmailsText = NSLocalizedString( + "Back in stock emails", + comment: "The title of one of the features offered with the Performance plan") + + static let marketingAutomationText = NSLocalizedString( + "Marketing automation", + comment: "The title of one of the features offered with the Performance plan") + + static let abandonedCartRecoveryText = NSLocalizedString( + "Abandoned cart recovery", + comment: "The title of one of the features offered with the Performance plan") + + static let referralProgramsText = NSLocalizedString( + "Referral programs", + comment: "The title of one of the features offered with the Performance plan") + + static let customerBirthdayEmailsText = NSLocalizedString( + "Customer birthday emails", + comment: "The title of one of the features offered with the Performance plan") + + static let loyaltyPointsProgramsText = NSLocalizedString( + "Loyalty points programs", + comment: "The title of one of the features offered with the Performance plan") + + static let discountedShippingText = NSLocalizedString( + "Discounted shipping²", + comment: "The title of one of the features offered with the Performance plan") } } @@ -309,7 +354,7 @@ private extension FullFeatureListView { "2. Only available in the U.S. – an additional extension will be required for other countries.", comment: "") } - + struct Layout { static let featureListSpacing: CGFloat = 8.0 static let featureListCornerRadius: CGFloat = 10.0 From 8038783673be866e4a60189c0612eb39150c773b Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 15:09:13 +0200 Subject: [PATCH 23/41] Extract feature list to its own viewmodel --- .../Upgrades/FullFeatureListView.swift | 275 +----------------- .../Upgrades/FullFeatureListViewModel.swift | 274 +++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 4 + 3 files changed, 280 insertions(+), 273 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index ab01e18a9ef..740dc7317f4 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -1,278 +1,6 @@ import Foundation import SwiftUI -struct FullFeatureListGroups { - public let title: String - public let essentialFeatures: [String] - public let performanceFeatures: [String] -} - -struct FullFeatureListViewModel { - static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { - return [ - FullFeatureListGroups(title: Localization.yourStoreFeatureTitle, - essentialFeatures: [ - Localization.wooCommerceStoreText, - Localization.wooCommerceMobileAppText, - Localization.wordPressCMSText, - Localization.wordPressMobileAppText, - Localization.freeSSLCertificateText, - Localization.generousStorageText, - Localization.automatedBackupQuickRestoreText, - Localization.adFreeExperienceText, - Localization.unlimitedAdminAccountsText, - Localization.liveChatSupportText, - Localization.emailSupportText, - Localization.premiumThemesIncludedText, - Localization.salesReportsText, - Localization.googleAnalyticsText, - ], - performanceFeatures: [] - ), - FullFeatureListGroups(title: Localization.productsFeatureTitle, - essentialFeatures: [ - Localization.listUnlimitedProducts, - Localization.giftCards, - Localization.listProductsByBrand, - ], - performanceFeatures: [ - Localization.minMaxOrderQuantityText, - Localization.productBundlesText, - Localization.customProductKitsText, - Localization.productRecommendationsText, - ]), - FullFeatureListGroups(title: Localization.paymentsFeatureTitle, - essentialFeatures: [ - Localization.integratedPayments, - Localization.internationalPayments, - Localization.automatedSalesTaxes, - Localization.acceptLocalPayments, - Localization.recurringPayments, - ], - performanceFeatures: []), - - FullFeatureListGroups(title: Localization.marketingAndEmailFeatureTitle, - essentialFeatures: [ - Localization.promoteOnTikTok, - Localization.syncWithPinterest, - Localization.connectWithFacebook, - Localization.advancedSeoTools, - Localization.advertiseOnGoogle, - Localization.customOrderEmails, - ], - performanceFeatures: [ - Localization.backInStockEmailsText, - Localization.marketingAutomationText, - Localization.abandonedCartRecoveryText, - Localization.referralProgramsText, - Localization.customerBirthdayEmailsText, - Localization.loyaltyPointsProgramsText, - ]), - - FullFeatureListGroups(title: Localization.shippingFeatureTitle, - essentialFeatures: [ - Localization.shipmentTracking, - Localization.liveShippingRates, - Localization.printShippingLabels - ], - performanceFeatures: [ - Localization.discountedShippingText, - ]), - ] - } -} - -private extension FullFeatureListViewModel { - struct Localization { - static let yourStoreFeatureTitle = NSLocalizedString( - "Your Store", - comment: "The title of one of the feature groups offered with paid plans") - - static let productsFeatureTitle = NSLocalizedString( - "Products", - comment: "The title of one of the feature groups offered with paid plans") - - static let paymentsFeatureTitle = NSLocalizedString( - "Payments", - comment: "The title of one of the feature groups offered with paid plans") - - static let marketingAndEmailFeatureTitle = NSLocalizedString( - "Marketing & Email", - comment: "The title of one of the feature groups offered with paid plans") - - static let shippingFeatureTitle = NSLocalizedString( - "Shipping", - comment: "The title of one of the feature groups offered with paid plans") - - static let wooCommerceStoreText = NSLocalizedString( - "WooCommerce store", - comment: "The title of one of the features offered with the Essential plan") - - static let wooCommerceMobileAppText = NSLocalizedString( - "WooCommerce mobile app", - comment: "The title of one of the features offered with the Essential plan") - - static let wordPressCMSText = NSLocalizedString( - "WordPress CMS", - comment: "The title of one of the features offered with the Essential plan") - - static let wordPressMobileAppText = NSLocalizedString( - "WordPress mobile app", - comment: "The title of one of the features offered with the Essential plan") - - static let freeSSLCertificateText = NSLocalizedString( - "Free SSL certificate", - comment: "The title of one of the features offered with the Essential plan") - - static let generousStorageText = NSLocalizedString( - "Generous storage", - comment: "The title of one of the features offered with the Essential plan") - - static let automatedBackupQuickRestoreText = NSLocalizedString( - "Automated backup + quick restore", - comment: "The title of one of the features offered with the Essential plan") - - static let adFreeExperienceText = NSLocalizedString( - "Ad-free experience", - comment: "The title of one of the features offered with the Essential plan") - - static let unlimitedAdminAccountsText = NSLocalizedString( - "Unlimited admin accounts", - comment: "The title of one of the features offered with the Essential plan") - - static let liveChatSupportText = NSLocalizedString( - "Live chat support", - comment: "The title of one of the features offered with the Essential plan") - - static let emailSupportText = NSLocalizedString( - "Email support", - comment: "The title of one of the features offered with the Essential plan") - - static let premiumThemesIncludedText = NSLocalizedString( - "Premium themes included", - comment: "The title of one of the features offered with the Essential plan") - - static let salesReportsText = NSLocalizedString( - "Sales reports", - comment: "The title of one of the features offered with the Essential plan") - - static let googleAnalyticsText = NSLocalizedString( - "Google Analytics", - comment: "The title of one of the features offered with the Essential plan") - - static let listUnlimitedProducts = NSLocalizedString( - "List unlimited products", - comment: "The title of one of the features offered with the Essential plan") - - static let giftCards = NSLocalizedString( - "Gift cards", - comment: "The title of one of the features offered with the Essential plan") - - static let listProductsByBrand = NSLocalizedString( - "List products by brand", - comment: "The title of one of the features offered with the Essential plan") - - static let integratedPayments = NSLocalizedString( - "Integrated payments", - comment: "The title of one of the features offered with the Essential plan") - - static let internationalPayments = NSLocalizedString( - "International payments'", - comment: "The title of one of the features offered with the Essential plan") - - static let automatedSalesTaxes = NSLocalizedString( - "Automated sales taxes", - comment: "The title of one of the features offered with the Essential plan") - - static let acceptLocalPayments = NSLocalizedString( - "Accept local payments'", - comment: "The title of one of the features offered with the Essential plan") - - static let recurringPayments = NSLocalizedString( - "Recurring payments'", - comment: "The title of one of the features offered with the Essential plan") - static let promoteOnTikTok = NSLocalizedString( - "Promote on TikTok", - comment: "The title of one of the features offered with the Essential plan") - - static let syncWithPinterest = NSLocalizedString( - "Sync with Pinterest", - comment: "The title of one of the features offered with the Essential plan") - - static let connectWithFacebook = NSLocalizedString( - "Connect with Facebook", - comment: "The title of one of the features offered with the Essential plan") - - static let advancedSeoTools = NSLocalizedString( - "Advanced SEO tools", - comment: "The title of one of the features offered with the Essential plan") - - static let advertiseOnGoogle = NSLocalizedString( - "Advertise on Google", - comment: "The title of one of the features offered with the Essential plan") - - static let customOrderEmails = NSLocalizedString( - "Custom order emails", - comment: "The title of one of the features offered with the Essential plan") - - static let shipmentTracking = NSLocalizedString( - "Shipment tracking", - comment: "The title of one of the features offered with the Essential plan") - - static let liveShippingRates = NSLocalizedString( - "Live shipping rates", - comment: "The title of one of the features offered with the Essential plan") - - static let printShippingLabels = NSLocalizedString( - "Print shipping labels²", - comment: "The title of one of the features offered with the Essential plan") - - static let minMaxOrderQuantityText = NSLocalizedString( - "Min/Max order quantity", - comment: "The title of one of the features offered with the Performance plan") - - static let productBundlesText = NSLocalizedString( - "Product Bundles", - comment: "The title of one of the features offered with the Performance plan") - - static let customProductKitsText = NSLocalizedString( - "Custom product kits", - comment: "The title of one of the features offered with the Performance plan") - - static let productRecommendationsText = NSLocalizedString( - "Product recommendations", - comment: "The title of one of the features offered with the Performance plan") - - static let backInStockEmailsText = NSLocalizedString( - "Back in stock emails", - comment: "The title of one of the features offered with the Performance plan") - - static let marketingAutomationText = NSLocalizedString( - "Marketing automation", - comment: "The title of one of the features offered with the Performance plan") - - static let abandonedCartRecoveryText = NSLocalizedString( - "Abandoned cart recovery", - comment: "The title of one of the features offered with the Performance plan") - - static let referralProgramsText = NSLocalizedString( - "Referral programs", - comment: "The title of one of the features offered with the Performance plan") - - static let customerBirthdayEmailsText = NSLocalizedString( - "Customer birthday emails", - comment: "The title of one of the features offered with the Performance plan") - - static let loyaltyPointsProgramsText = NSLocalizedString( - "Loyalty points programs", - comment: "The title of one of the features offered with the Performance plan") - - static let discountedShippingText = NSLocalizedString( - "Discounted shipping²", - comment: "The title of one of the features offered with the Performance plan") - } -} - struct FullFeatureListView: View { @Environment(\.presentationMode) var presentationMode @@ -285,6 +13,7 @@ struct FullFeatureListView: View { Text(featureList.title) .font(.title) .bold() + .padding(.top) .padding(.bottom) ForEach(featureList.essentialFeatures, id: \.self) { feature in Text(feature) @@ -356,7 +85,7 @@ private extension FullFeatureListView { } struct Layout { - static let featureListSpacing: CGFloat = 8.0 + static let featureListSpacing: CGFloat = 16.0 static let featureListCornerRadius: CGFloat = 10.0 } } diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift new file mode 100644 index 00000000000..298988df25c --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift @@ -0,0 +1,274 @@ +import Foundation +import SwiftUI + +struct FullFeatureListGroups { + public let title: String + public let essentialFeatures: [String] + public let performanceFeatures: [String] +} + +struct FullFeatureListViewModel { + static func hardcodedFullFeatureList() -> [FullFeatureListGroups] { + return [ + FullFeatureListGroups(title: Localization.yourStoreFeatureTitle, + essentialFeatures: [ + Localization.wooCommerceStoreText, + Localization.wooCommerceMobileAppText, + Localization.wordPressCMSText, + Localization.wordPressMobileAppText, + Localization.freeSSLCertificateText, + Localization.generousStorageText, + Localization.automatedBackupQuickRestoreText, + Localization.adFreeExperienceText, + Localization.unlimitedAdminAccountsText, + Localization.liveChatSupportText, + Localization.emailSupportText, + Localization.premiumThemesIncludedText, + Localization.salesReportsText, + Localization.googleAnalyticsText, + ], + performanceFeatures: [] + ), + FullFeatureListGroups(title: Localization.productsFeatureTitle, + essentialFeatures: [ + Localization.listUnlimitedProducts, + Localization.giftCards, + Localization.listProductsByBrand, + ], + performanceFeatures: [ + Localization.minMaxOrderQuantityText, + Localization.productBundlesText, + Localization.customProductKitsText, + Localization.productRecommendationsText, + ]), + FullFeatureListGroups(title: Localization.paymentsFeatureTitle, + essentialFeatures: [ + Localization.integratedPayments, + Localization.internationalPayments, + Localization.automatedSalesTaxes, + Localization.acceptLocalPayments, + Localization.recurringPayments, + ], + performanceFeatures: []), + + FullFeatureListGroups(title: Localization.marketingAndEmailFeatureTitle, + essentialFeatures: [ + Localization.promoteOnTikTok, + Localization.syncWithPinterest, + Localization.connectWithFacebook, + Localization.advancedSeoTools, + Localization.advertiseOnGoogle, + Localization.customOrderEmails, + ], + performanceFeatures: [ + Localization.backInStockEmailsText, + Localization.marketingAutomationText, + Localization.abandonedCartRecoveryText, + Localization.referralProgramsText, + Localization.customerBirthdayEmailsText, + Localization.loyaltyPointsProgramsText, + ]), + + FullFeatureListGroups(title: Localization.shippingFeatureTitle, + essentialFeatures: [ + Localization.shipmentTracking, + Localization.liveShippingRates, + Localization.printShippingLabels + ], + performanceFeatures: [ + Localization.discountedShippingText, + ]), + ] + } +} + +private extension FullFeatureListViewModel { + struct Localization { + static let yourStoreFeatureTitle = NSLocalizedString( + "Your Store", + comment: "The title of one of the feature groups offered with paid plans") + + static let productsFeatureTitle = NSLocalizedString( + "Products", + comment: "The title of one of the feature groups offered with paid plans") + + static let paymentsFeatureTitle = NSLocalizedString( + "Payments", + comment: "The title of one of the feature groups offered with paid plans") + + static let marketingAndEmailFeatureTitle = NSLocalizedString( + "Marketing & Email", + comment: "The title of one of the feature groups offered with paid plans") + + static let shippingFeatureTitle = NSLocalizedString( + "Shipping", + comment: "The title of one of the feature groups offered with paid plans") + + static let wooCommerceStoreText = NSLocalizedString( + "WooCommerce store", + comment: "The title of one of the features offered with the Essential plan") + + static let wooCommerceMobileAppText = NSLocalizedString( + "WooCommerce mobile app", + comment: "The title of one of the features offered with the Essential plan") + + static let wordPressCMSText = NSLocalizedString( + "WordPress CMS", + comment: "The title of one of the features offered with the Essential plan") + + static let wordPressMobileAppText = NSLocalizedString( + "WordPress mobile app", + comment: "The title of one of the features offered with the Essential plan") + + static let freeSSLCertificateText = NSLocalizedString( + "Free SSL certificate", + comment: "The title of one of the features offered with the Essential plan") + + static let generousStorageText = NSLocalizedString( + "Generous storage", + comment: "The title of one of the features offered with the Essential plan") + + static let automatedBackupQuickRestoreText = NSLocalizedString( + "Automated backup + quick restore", + comment: "The title of one of the features offered with the Essential plan") + + static let adFreeExperienceText = NSLocalizedString( + "Ad-free experience", + comment: "The title of one of the features offered with the Essential plan") + + static let unlimitedAdminAccountsText = NSLocalizedString( + "Unlimited admin accounts", + comment: "The title of one of the features offered with the Essential plan") + + static let liveChatSupportText = NSLocalizedString( + "Live chat support", + comment: "The title of one of the features offered with the Essential plan") + + static let emailSupportText = NSLocalizedString( + "Email support", + comment: "The title of one of the features offered with the Essential plan") + + static let premiumThemesIncludedText = NSLocalizedString( + "Premium themes included", + comment: "The title of one of the features offered with the Essential plan") + + static let salesReportsText = NSLocalizedString( + "Sales reports", + comment: "The title of one of the features offered with the Essential plan") + + static let googleAnalyticsText = NSLocalizedString( + "Google Analytics", + comment: "The title of one of the features offered with the Essential plan") + + static let listUnlimitedProducts = NSLocalizedString( + "List unlimited products", + comment: "The title of one of the features offered with the Essential plan") + + static let giftCards = NSLocalizedString( + "Gift cards", + comment: "The title of one of the features offered with the Essential plan") + + static let listProductsByBrand = NSLocalizedString( + "List products by brand", + comment: "The title of one of the features offered with the Essential plan") + + static let integratedPayments = NSLocalizedString( + "Integrated payments", + comment: "The title of one of the features offered with the Essential plan") + + static let internationalPayments = NSLocalizedString( + "International payments'", + comment: "The title of one of the features offered with the Essential plan") + + static let automatedSalesTaxes = NSLocalizedString( + "Automated sales taxes", + comment: "The title of one of the features offered with the Essential plan") + + static let acceptLocalPayments = NSLocalizedString( + "Accept local payments'", + comment: "The title of one of the features offered with the Essential plan") + + static let recurringPayments = NSLocalizedString( + "Recurring payments'", + comment: "The title of one of the features offered with the Essential plan") + static let promoteOnTikTok = NSLocalizedString( + "Promote on TikTok", + comment: "The title of one of the features offered with the Essential plan") + + static let syncWithPinterest = NSLocalizedString( + "Sync with Pinterest", + comment: "The title of one of the features offered with the Essential plan") + + static let connectWithFacebook = NSLocalizedString( + "Connect with Facebook", + comment: "The title of one of the features offered with the Essential plan") + + static let advancedSeoTools = NSLocalizedString( + "Advanced SEO tools", + comment: "The title of one of the features offered with the Essential plan") + + static let advertiseOnGoogle = NSLocalizedString( + "Advertise on Google", + comment: "The title of one of the features offered with the Essential plan") + + static let customOrderEmails = NSLocalizedString( + "Custom order emails", + comment: "The title of one of the features offered with the Essential plan") + + static let shipmentTracking = NSLocalizedString( + "Shipment tracking", + comment: "The title of one of the features offered with the Essential plan") + + static let liveShippingRates = NSLocalizedString( + "Live shipping rates", + comment: "The title of one of the features offered with the Essential plan") + + static let printShippingLabels = NSLocalizedString( + "Print shipping labels²", + comment: "The title of one of the features offered with the Essential plan") + + static let minMaxOrderQuantityText = NSLocalizedString( + "Min/Max order quantity", + comment: "The title of one of the features offered with the Performance plan") + + static let productBundlesText = NSLocalizedString( + "Product Bundles", + comment: "The title of one of the features offered with the Performance plan") + + static let customProductKitsText = NSLocalizedString( + "Custom product kits", + comment: "The title of one of the features offered with the Performance plan") + + static let productRecommendationsText = NSLocalizedString( + "Product recommendations", + comment: "The title of one of the features offered with the Performance plan") + + static let backInStockEmailsText = NSLocalizedString( + "Back in stock emails", + comment: "The title of one of the features offered with the Performance plan") + + static let marketingAutomationText = NSLocalizedString( + "Marketing automation", + comment: "The title of one of the features offered with the Performance plan") + + static let abandonedCartRecoveryText = NSLocalizedString( + "Abandoned cart recovery", + comment: "The title of one of the features offered with the Performance plan") + + static let referralProgramsText = NSLocalizedString( + "Referral programs", + comment: "The title of one of the features offered with the Performance plan") + + static let customerBirthdayEmailsText = NSLocalizedString( + "Customer birthday emails", + comment: "The title of one of the features offered with the Performance plan") + + static let loyaltyPointsProgramsText = NSLocalizedString( + "Loyalty points programs", + comment: "The title of one of the features offered with the Performance plan") + + static let discountedShippingText = NSLocalizedString( + "Discounted shipping²", + comment: "The title of one of the features offered with the Performance plan") + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index f6d40166995..00ac5cf22a9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1258,6 +1258,7 @@ 6879B8DB287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */; }; 6881CCC42A5EE6BF00AEDE36 /* WooPlanCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */; }; 6888A2C82A668D650026F5C0 /* FullFeatureListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */; }; + 6888A2CA2A66C42C0026F5C0 /* FullFeatureListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888A2C92A66C42C0026F5C0 /* FullFeatureListViewModel.swift */; }; 68D1BEDB28FFEDC20074A29E /* OrderCustomerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D1BEDA28FFEDC20074A29E /* OrderCustomerListView.swift */; }; 68D1BEDD2900E4180074A29E /* CustomerSearchUICommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D1BEDC2900E4180074A29E /* CustomerSearchUICommand.swift */; }; 68E6749F2A4DA01C0034BA1E /* WooWPComPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */; }; @@ -3621,6 +3622,7 @@ 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsViewModelTests.swift; sourceTree = ""; }; 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPlanCardView.swift; sourceTree = ""; }; 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullFeatureListView.swift; sourceTree = ""; }; + 6888A2C92A66C42C0026F5C0 /* FullFeatureListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullFeatureListViewModel.swift; sourceTree = ""; }; 68D1BEDA28FFEDC20074A29E /* OrderCustomerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCustomerListView.swift; sourceTree = ""; }; 68D1BEDC2900E4180074A29E /* CustomerSearchUICommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSearchUICommand.swift; sourceTree = ""; }; 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooWPComPlan.swift; sourceTree = ""; }; @@ -6443,6 +6445,7 @@ 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */, 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */, 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */, + 6888A2C92A66C42C0026F5C0 /* FullFeatureListViewModel.swift */, ); name = Upgrades; path = Classes/ViewRelated/Upgrades; @@ -11604,6 +11607,7 @@ EEBDF7DA2A2EF69B00EFEF47 /* ShareProductCoordinator.swift in Sources */, DEFB3011289904E300A620B3 /* WooSetupWebViewModel.swift in Sources */, 022F2FA8295E7A14003A0A46 /* StoreCreationCountryButton.swift in Sources */, + 6888A2CA2A66C42C0026F5C0 /* FullFeatureListViewModel.swift in Sources */, 02E8B17A23E2C4BD00A43403 /* CircleSpinnerView.swift in Sources */, CCE4CD282669324300E09FD4 /* ShippingLabelPaymentMethodsTopBanner.swift in Sources */, 269B46622A16D68A00ADA872 /* UpdateAnalyticsSettingsUseCase.swift in Sources */, From 29a9bda09d9af3f167b740314b89bd5809799365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 15:11:36 +0200 Subject: [PATCH 24/41] Add CouponListViewController properties to the initializer for an easier creation process --- .../ViewRelated/Coupons/CouponListView.swift | 10 ++--- .../Coupons/CouponListViewController.swift | 45 +++++++++++-------- .../EnhancedCouponListViewController.swift | 20 +++++---- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift index cfc39078c27..97659297ff7 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListView.swift @@ -7,12 +7,12 @@ struct CouponListView: UIViewControllerRepresentable { let emptyStateAction: (() -> Void) let onCouponSelected: ((Coupon) -> Void) - func makeUIViewController(context: Self.Context) -> CouponListViewController { - let viewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: false) - viewController.emptyStateActionTitle = emptyStateActionTitle - viewController.onCouponSelected = onCouponSelected - viewController.emptyStateAction = emptyStateAction + let viewController = CouponListViewController(siteID: siteID, + showFeedbackBannerIfAppropriate: false, + emptyStateActionTitle: emptyStateActionTitle, + emptyStateAction: emptyStateAction, + onCouponSelected: onCouponSelected) return viewController } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift index 7cfd11dc267..40a184c0ef5 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift @@ -38,14 +38,23 @@ final class CouponListViewController: UIViewController, GhostableViewController private lazy var dataSource: UITableViewDiffableDataSource = makeDataSource() private lazy var topBannerView: TopBannerView = createFeedbackBannerView() - var onDataLoaded: ((Bool) -> Void)? - var emptyStateAction: (() -> Void)? - var emptyStateActionTitle: String? - var onCouponSelected: ((Coupon) -> Void)? - - init(siteID: Int64, showFeedbackBannerIfAppropriate: Bool) { + private var onDataLoaded: ((Bool) -> Void)? + private let emptyStateAction: (() -> Void) + private let emptyStateActionTitle: String + private let onCouponSelected: ((Coupon) -> Void) + + init(siteID: Int64, + showFeedbackBannerIfAppropriate: Bool, + emptyStateActionTitle: String, + onDataLoaded: ((Bool) -> Void)? = nil, + emptyStateAction: @escaping (() -> Void), + onCouponSelected: @escaping ((Coupon) -> Void)) { self.siteID = siteID self.viewModel = CouponListViewModel(siteID: siteID, showFeedbackBannerIfAppropriate: showFeedbackBannerIfAppropriate) + self.onDataLoaded = onDataLoaded + self.emptyStateAction = emptyStateAction + self.emptyStateActionTitle = emptyStateActionTitle + self.onCouponSelected = onCouponSelected super.init(nibName: type(of: self).nibName, bundle: nil) } @@ -194,7 +203,7 @@ extension CouponListViewController: UITableViewDelegate { return } - onCouponSelected?(coupon) + onCouponSelected(coupon) } } @@ -289,19 +298,17 @@ private extension CouponListViewController { let emptyStateViewController = EmptyStateViewController(style: .list) displayEmptyStateViewController(emptyStateViewController) - if let emptyStateCreateCouponAction = emptyStateAction, - let emptyStateActionTitle = emptyStateActionTitle { - let configuration = EmptyStateViewController.Config.withButton( - message: .init(string: Localization.couponCreationSuggestionMessage), - image: .emptyCouponsImage, - details: Localization.emptyStateDetails, - buttonTitle: emptyStateActionTitle - ) { _ in - emptyStateCreateCouponAction() - } - - emptyStateViewController.configure(configuration) + let configuration = EmptyStateViewController.Config.withButton( + message: .init(string: Localization.couponCreationSuggestionMessage), + image: .emptyCouponsImage, + details: Localization.emptyStateDetails, + buttonTitle: emptyStateActionTitle + ) { [weak self] _ in + self?.emptyStateAction() } + + emptyStateViewController.configure(configuration) + } diff --git a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift index 8ace10fd543..32ade11ed79 100644 --- a/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Coupons/EnhancedCouponListViewController.swift @@ -6,7 +6,7 @@ import Yosemite /// Shows a coupons list plus the entry to other accessory actions: search, creation. /// final class EnhancedCouponListViewController: UIViewController { - private let couponListViewController: CouponListViewController + private var couponListViewController: CouponListViewController? private let siteID: Int64 private lazy var noticePresenter: DefaultNoticePresenter = { @@ -17,14 +17,8 @@ final class EnhancedCouponListViewController: UIViewController { init(siteID: Int64) { self.siteID = siteID - couponListViewController = CouponListViewController(siteID: siteID, showFeedbackBannerIfAppropriate: true) super.init(nibName: nil, bundle: nil) - - couponListViewController.onDataLoaded = configureNavigationBarItems - couponListViewController.emptyStateAction = displayCouponTypeBottomSheet - couponListViewController.emptyStateActionTitle = Localization.createCouponAction - couponListViewController.onCouponSelected = showDetails } required init?(coder: NSCoder) { @@ -71,11 +65,19 @@ final class EnhancedCouponListViewController: UIViewController { private extension EnhancedCouponListViewController { func configureCouponListViewController() { + let couponListViewController = CouponListViewController(siteID: siteID, + showFeedbackBannerIfAppropriate: true, + emptyStateActionTitle: Localization.createCouponAction, + emptyStateAction: displayCouponTypeBottomSheet, + onCouponSelected: showDetails) + couponListViewController.view.translatesAutoresizingMaskIntoConstraints = false addChild(couponListViewController) view.addSubview(couponListViewController.view) view.pinSubviewToAllEdges(couponListViewController.view) couponListViewController.didMove(toParent: self) + + self.couponListViewController = couponListViewController } func configureNavigation() { @@ -110,7 +112,7 @@ private extension EnhancedCouponListViewController { let viewModel = AddEditCouponViewModel(siteID: siteID, discountType: discountType, onSuccess: { [weak self] _ in - self?.couponListViewController.refreshCouponList() + self?.couponListViewController?.refreshCouponList() }) let addEditHostingController = AddEditCouponHostingController(viewModel: viewModel, onDisappear: { [weak self] in guard let self = self else { return } @@ -136,7 +138,7 @@ private extension EnhancedCouponListViewController { func showDetails(from coupon: Coupon) { let detailsViewModel = CouponDetailsViewModel(coupon: coupon, onUpdate: { [weak self] in guard let self = self else { return } - self.couponListViewController.refreshCouponList() + self.couponListViewController?.refreshCouponList() }, onDeletion: { [weak self] in guard let self = self else { return } self.navigationController?.popViewController(animated: true) From dc19c9fe1ce555cf4d619f53136625eb891d7f3f Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Jul 2023 15:12:02 +0200 Subject: [PATCH 25/41] Update missing localized comments --- .../Classes/ViewRelated/Upgrades/FullFeatureListView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index 740dc7317f4..a9a58157ea9 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -68,7 +68,7 @@ private extension FullFeatureListView { struct Localization { static let featureListTitleText = NSLocalizedString( "Full Feature List", - comment: "") + comment: "Title of the view which shows the full feature list for paid plans.") static let performanceOnlyText = NSLocalizedString( "Performance plan only", @@ -77,11 +77,11 @@ private extension FullFeatureListView { static let paymentsDisclaimerText = NSLocalizedString( "1. Available as standard in WooCommerce Payments (restrictions apply)." + "Additional extensions may be required for other payment providers." , - comment: "") + comment: "Disclaimer regarding some of the features related to payments.") static let pluginsDisclaimerText = NSLocalizedString( "2. Only available in the U.S. – an additional extension will be required for other countries.", - comment: "") + comment: "Disclaimer regarding some of the features related to shipping.") } struct Layout { From db5ac8d3b0bc2cbfaeb97346af19138403fdd9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 16:48:57 +0200 Subject: [PATCH 26/41] Add alert before going to coupons. --- .../PaymentSection/OrderPaymentSection.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index ef90966d58b..425edc3d95f 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -18,6 +18,10 @@ struct OrderPaymentSection: View { /// @State private var shouldShowAddCouponLineDetails: Bool = false + /// Indicates if the go to coupons alert should be shown or not. + /// + @State private var shouldShowGoToCouponsAlert: Bool = false + /// Keeps track of the selected coupon line details view model. /// @State private var selectedCouponLineDetailsViewModel: CouponLineDetailsViewModel? = nil @@ -74,7 +78,7 @@ struct OrderPaymentSection: View { CouponListView(siteID: viewModel.siteID, emptyStateActionTitle: Localization.goToCoupons, emptyStateAction: { - MainTabBarController.presentCoupons() + shouldShowGoToCouponsAlert = true }, onCouponSelected: { coupon in viewModel.addNewCouponLineClosure(coupon) @@ -89,6 +93,14 @@ struct OrderPaymentSection: View { } } } + .alert(isPresented: $shouldShowGoToCouponsAlert, content: { + Alert(title: Text(Localization.goToCoupons), + message: Text(Localization.goToCouponsAlertMessage), + primaryButton: .default(Text(Localization.goToCouponsAlertButtonTitle), action: { + MainTabBarController.presentCoupons() + }), + secondaryButton: .cancel()) + }) } } @@ -162,6 +174,9 @@ private extension OrderPaymentSection { static let coupon = NSLocalizedString("Coupon", comment: "Label for the row showing the cost of coupon in the order") static let goToCoupons = NSLocalizedString("Go to Coupons", comment: "Button title on the Coupon screen empty state" + "when creating a new order that navigates to the Coupons Section") + static let goToCouponsAlertMessage = NSLocalizedString("Do you want to navigate to the Coupons Menu? These changes will be discarded.", + comment: "Confirm message for navigating to coupons when creating a new order") + static let goToCouponsAlertButtonTitle = NSLocalizedString("Go", comment: "Confirm buton titele for navigating to coupons when creating a new order") static let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title when showing the coupon list selector") } } From 9bdb8747e95560debc3a1457611711a5b9de217b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 17:49:16 +0200 Subject: [PATCH 27/41] Add release note. --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ff578d97604..200f4eaef95 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ 14.6 ----- - [Internal] Media picker flow was refactored to support interactive dismissal for device photo picker and WordPress media picker sources. Affected flows: product form > images, and virtual product form > downloadable files. [https://github.com/woocommerce/woocommerce-ios/pull/10236] +- [*] Orders with Coupons: Users can now select a coupon from a list when adding it to an order. [https://github.com/woocommerce/woocommerce-ios/pull/10255] 14.5 From 4d9987a0840815c3b80bbd73de95369fc56ae3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Tue, 18 Jul 2023 17:50:09 +0200 Subject: [PATCH 28/41] Remove empty line. --- WooCommerce/Classes/ViewRelated/MainTabBarController.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index b213641dcfb..4ab6d5a8bac 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -454,7 +454,6 @@ extension MainTabBarController { static func presentCoupons() { switchToHubMenuTab() - guard let hubMenuViewController: HubMenuViewController = childViewController() else { return } From 4bb411da36f3ab3d05a3ff083b9f88a27172175f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Carri=C3=B3n?= Date: Tue, 18 Jul 2023 17:31:42 -0500 Subject: [PATCH 29/41] Encapsulates application password encoding login into a `ApplicationPasswordEncoder` type --- .../Networking.xcodeproj/project.pbxproj | 8 +++++ .../AppicationPasswordEncoder.swift | 32 +++++++++++++++++++ .../ApplicationPasswordEncoderTests.swift | 22 +++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift create mode 100644 Networking/NetworkingTests/ApplicationPassword/ApplicationPasswordEncoderTests.swift diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index a1054a3c414..761263822ad 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -172,6 +172,8 @@ 24F98C602502EF8200F49B68 /* FeatureFlagRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */; }; 24F98C622502EFF600F49B68 /* feature-flags-load-all.json in Resources */ = {isa = PBXBuildFile; fileRef = 24F98C612502EFF600F49B68 /* feature-flags-load-all.json */; }; 261870782540A252006522A1 /* ShippingLineTax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261870772540A252006522A1 /* ShippingLineTax.swift */; }; + 261C466B2A6738EE00734070 /* AppicationPasswordEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */; }; + 261C466D2A67478800734070 /* ApplicationPasswordEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */; }; 261CF1B4255AD6B30090D8D3 /* payment-gateway-list.json in Resources */ = {isa = PBXBuildFile; fileRef = 261CF1B3255AD6B30090D8D3 /* payment-gateway-list.json */; }; 261CF1B8255AE62D0090D8D3 /* PaymentGatewayRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1B7255AE62D0090D8D3 /* PaymentGatewayRemote.swift */; }; 261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */; }; @@ -1122,6 +1124,8 @@ 24F98C5F2502EF8200F49B68 /* FeatureFlagRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagRemoteTests.swift; sourceTree = ""; }; 24F98C612502EFF600F49B68 /* feature-flags-load-all.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "feature-flags-load-all.json"; sourceTree = ""; }; 261870772540A252006522A1 /* ShippingLineTax.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLineTax.swift; sourceTree = ""; }; + 261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppicationPasswordEncoder.swift; sourceTree = ""; }; + 261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordEncoderTests.swift; sourceTree = ""; }; 261CF1B3255AD6B30090D8D3 /* payment-gateway-list.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-list.json"; sourceTree = ""; }; 261CF1B7255AE62D0090D8D3 /* PaymentGatewayRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayRemote.swift; sourceTree = ""; }; 261CF1BB255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsGatewayRemoteTests.swift; sourceTree = ""; }; @@ -3166,6 +3170,7 @@ EE54C89E2947782E00A9BF61 /* ApplicationPasswordUseCase.swift */, DEEF8E6729C858AD00D47411 /* OneTimeApplicationPasswordUseCase.swift */, EE71CC3C2951A8EA0074D908 /* ApplicationPasswordStorage.swift */, + 261C466A2A6738EE00734070 /* AppicationPasswordEncoder.swift */, EE99814D295AA7430074AE68 /* RequestAuthenticator.swift */, EE99814F295AACE10074AE68 /* RequestConverter.swift */, ); @@ -3233,6 +3238,7 @@ EE8DE431294B17CD005054E7 /* DefaultApplicationPasswordUseCaseTests.swift */, EE76762E2962B85E000066FA /* RequestProcessorTests.swift */, 45C6D0E329B9F327009CE29C /* CookieNonceAuthenticatorTests.swift */, + 261C466C2A67478800734070 /* ApplicationPasswordEncoderTests.swift */, ); path = ApplicationPassword; sourceTree = ""; @@ -4083,6 +4089,7 @@ 0359EA1727AAC7740048DE2D /* WCPayCardFunding.swift in Sources */, D88E229425AC9B420023F3B1 /* OrderFeeTaxStatus.swift in Sources */, B5DAEFF02180DD5A0002356A /* NotificationsRemote.swift in Sources */, + 261C466B2A6738EE00734070 /* AppicationPasswordEncoder.swift in Sources */, 2665032A261F41510079A159 /* ProductAddOn.swift in Sources */, 020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */, 0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */, @@ -4325,6 +4332,7 @@ 45ED4F14239E8F2E004F1BE3 /* TaxClassRemoteTests.swift in Sources */, D88D5A4D230BD010007B6E01 /* ProductReviewsRemoteTests.swift in Sources */, 261CF1BC255AEE290090D8D3 /* PaymentsGatewayRemoteTests.swift in Sources */, + 261C466D2A67478800734070 /* ApplicationPasswordEncoderTests.swift in Sources */, 453305EF2459E46100264E50 /* SitePostsRemoteTests.swift in Sources */, 45C6D0E429B9F327009CE29C /* CookieNonceAuthenticatorTests.swift in Sources */, 24F98C602502EF8200F49B68 /* FeatureFlagRemoteTests.swift in Sources */, diff --git a/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift b/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift new file mode 100644 index 00000000000..3c35ca0034e --- /dev/null +++ b/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Utility class to encode the stored application password. +/// By default it uses the stored application password. +/// +public struct ApplicationPasswordEncoder { + + /// Password envelope. + /// + private let passwordEnvelope: ApplicationPassword? + + init(passwordEnvelope: ApplicationPassword? = ApplicationPasswordStorage().applicationPassword) { + self.passwordEnvelope = passwordEnvelope + } + + /// Returns the application password on a base64 encoded format. + /// The output is ready to be used in the authentication header. + /// Returns `nil` if the password can't be encoded. + /// + public func encodedPassword() -> String? { + guard let passwordEnvelope else { + return nil + } + + let loginString = "\(passwordEnvelope.wpOrgUsername):\(passwordEnvelope.password.secretValue)" + guard let loginData = loginString.data(using: .utf8) else { + return nil + } + + return loginData.base64EncodedString() + } +} diff --git a/Networking/NetworkingTests/ApplicationPassword/ApplicationPasswordEncoderTests.swift b/Networking/NetworkingTests/ApplicationPassword/ApplicationPasswordEncoderTests.swift new file mode 100644 index 00000000000..f8a594c0d72 --- /dev/null +++ b/Networking/NetworkingTests/ApplicationPassword/ApplicationPasswordEncoderTests.swift @@ -0,0 +1,22 @@ +import XCTest +@testable import Networking + +final class ApplicationPasswordEncoderTests: XCTestCase { + + func test_nil_password_envelope_returns_nil_password() { + let encoder = ApplicationPasswordEncoder(passwordEnvelope: nil) + XCTAssertNil(encoder.encodedPassword()) + } + + func test_sample_password_envelope_returns_encoded_password() { + // Given + let envelope = ApplicationPassword(wpOrgUsername: "This", password: .init("is-a-test"), uuid: "") + + // When + let encoder = ApplicationPasswordEncoder(passwordEnvelope: envelope) + + // Then + let expected = "VGhpczppcy1hLXRlc3Q=" /// `This:is-a-test` encoded with https://www.base64encode.org/ + XCTAssertEqual(encoder.encodedPassword(), expected) + } +} From 2d42a1235624f70e60e9f1ed09462d459c6964a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Carri=C3=B3n?= Date: Tue, 18 Jul 2023 17:32:33 -0500 Subject: [PATCH 30/41] Replace encoding logic with the new ApplicationPasswordEncoder type --- .../OneTimeApplicationPasswordUseCase.swift | 8 ++------ .../Networking/Requests/AuthenticatedRESTRequest.swift | 7 +------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/Networking/Networking/ApplicationPassword/OneTimeApplicationPasswordUseCase.swift b/Networking/Networking/ApplicationPassword/OneTimeApplicationPasswordUseCase.swift index 3a6f18d2be6..229cdfe4bf3 100644 --- a/Networking/Networking/ApplicationPassword/OneTimeApplicationPasswordUseCase.swift +++ b/Networking/Networking/ApplicationPassword/OneTimeApplicationPasswordUseCase.swift @@ -61,18 +61,14 @@ private extension OneTimeApplicationPasswordUseCase { } func authenticateRequest(request: URLRequest) -> URLRequest { - guard let username = applicationPassword?.wpOrgUsername, - let password = applicationPassword?.password.secretValue else { + guard let applicationPassword else { return request } var authenticatedRequest = request authenticatedRequest.setValue("application/json", forHTTPHeaderField: "Accept") authenticatedRequest.setValue(UserAgent.defaultUserAgent, forHTTPHeaderField: "User-Agent") - let loginString = "\(username):\(password)" - - if let loginData = loginString.data(using: .utf8) { - let base64LoginString = loginData.base64EncodedString() + if let base64LoginString = ApplicationPasswordEncoder(passwordEnvelope: applicationPassword).encodedPassword() { authenticatedRequest.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") } diff --git a/Networking/Networking/Requests/AuthenticatedRESTRequest.swift b/Networking/Networking/Requests/AuthenticatedRESTRequest.swift index 47159cb0a36..4a847b949e6 100644 --- a/Networking/Networking/Requests/AuthenticatedRESTRequest.swift +++ b/Networking/Networking/Requests/AuthenticatedRESTRequest.swift @@ -14,12 +14,7 @@ struct AuthenticatedRESTRequest: URLRequestConvertible { authenticated.setValue("application/json", forHTTPHeaderField: "Accept") authenticated.setValue(UserAgent.defaultUserAgent, forHTTPHeaderField: "User-Agent") - let username = applicationPassword.wpOrgUsername - let password = applicationPassword.password.secretValue - let loginString = "\(username):\(password)" - - if let loginData = loginString.data(using: .utf8) { - let base64LoginString = loginData.base64EncodedString() + if let base64LoginString = ApplicationPasswordEncoder(passwordEnvelope: applicationPassword).encodedPassword() { authenticated.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization") } From 2b5174b34c6e44fa6ca8344100224a2805ecbe67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Carri=C3=B3n?= Date: Tue, 18 Jul 2023 17:42:44 -0500 Subject: [PATCH 31/41] make init public --- .../ApplicationPassword/AppicationPasswordEncoder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift b/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift index 3c35ca0034e..625a69f07e2 100644 --- a/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift +++ b/Networking/Networking/ApplicationPassword/AppicationPasswordEncoder.swift @@ -9,8 +9,8 @@ public struct ApplicationPasswordEncoder { /// private let passwordEnvelope: ApplicationPassword? - init(passwordEnvelope: ApplicationPassword? = ApplicationPasswordStorage().applicationPassword) { - self.passwordEnvelope = passwordEnvelope + public init(passwordEnvelope: ApplicationPassword? = nil) { + self.passwordEnvelope = passwordEnvelope ?? ApplicationPasswordStorage().applicationPassword } /// Returns the application password on a base64 encoded format. From b18ae1bbe5e450db72d5eb6a6df39fa157f95ddd Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 19 Jul 2023 10:35:41 +0530 Subject: [PATCH 32/41] Inject ABTestVariationProvider for unit test purposes. --- .../AddProductFromImageEligibilityChecker.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift index c9e2dfb61d2..022099f9b4d 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift @@ -16,11 +16,14 @@ protocol AddProductFromImageEligibilityCheckerProtocol { final class AddProductFromImageEligibilityChecker: AddProductFromImageEligibilityCheckerProtocol { private let stores: StoresManager private let featureFlagService: FeatureFlagService + private let abTestVariationProvider: ABTestVariationProvider init(stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, + abTestVariationProvider: ABTestVariationProvider = DefaultABTestVariationProvider()) { self.stores = stores self.featureFlagService = featureFlagService + self.abTestVariationProvider = abTestVariationProvider } func isEligibleToParticipateInABTest() -> Bool { @@ -39,6 +42,6 @@ final class AddProductFromImageEligibilityChecker: AddProductFromImageEligibilit return false } - return ABTest.addProductFromImage.variation == .treatment + return abTestVariationProvider.variation(for: .addProductFromImage) == .treatment } } From 118f480e6c9a7c354d59e03c22e7196c9ef0b952 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 19 Jul 2023 10:36:11 +0530 Subject: [PATCH 33/41] Update unit tests to include AB test variation check. --- ...ductFromImageEligibilityCheckerTests.swift | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift index 599c427a239..c8a9e75e9d6 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift @@ -1,5 +1,6 @@ import TestKit import XCTest +import Experiments @testable import WooCommerce @@ -46,9 +47,14 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { func test_isEligible_is_true_for_wpcom_store() throws { // Given + let mockABTestVariationProvider = MockABTestVariationProvider() + mockABTestVariationProvider.mockVariationValue = .treatment + updateDefaultStore(isWPCOMStore: true) let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) - let checker = AddProductFromImageEligibilityChecker(stores: stores, featureFlagService: featureFlagService) + let checker = AddProductFromImageEligibilityChecker(stores: stores, + featureFlagService: featureFlagService, + abTestVariationProvider: mockABTestVariationProvider) // When let isEligible = checker.isEligible() @@ -59,9 +65,14 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { func test_isEligible_is_false_for_non_wpcom_store() throws { // Given + let mockABTestVariationProvider = MockABTestVariationProvider() + mockABTestVariationProvider.mockVariationValue = .treatment + updateDefaultStore(isWPCOMStore: false) let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) - let checker = AddProductFromImageEligibilityChecker(stores: stores, featureFlagService: featureFlagService) + let checker = AddProductFromImageEligibilityChecker(stores: stores, + featureFlagService: featureFlagService, + abTestVariationProvider: mockABTestVariationProvider) // When let isEligible = checker.isEligible() @@ -72,9 +83,32 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { func test_isEligible_is_false_for_wpcom_store_when_feature_flag_is_disabled() throws { // Given + let mockABTestVariationProvider = MockABTestVariationProvider() + mockABTestVariationProvider.mockVariationValue = .treatment + updateDefaultStore(isWPCOMStore: true) let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: false) - let checker = AddProductFromImageEligibilityChecker(stores: stores, featureFlagService: featureFlagService) + let checker = AddProductFromImageEligibilityChecker(stores: stores, + featureFlagService: featureFlagService, + abTestVariationProvider: mockABTestVariationProvider) + + // When + let isEligible = checker.isEligible() + + // Then + XCTAssertFalse(isEligible) + } + + func test_isEligible_is_false_for_wpcom_store_when_ab_test_variation_is_control() throws { + // Given + let mockABTestVariationProvider = MockABTestVariationProvider() + mockABTestVariationProvider.mockVariationValue = .control + + updateDefaultStore(isWPCOMStore: true) + let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) + let checker = AddProductFromImageEligibilityChecker(stores: stores, + featureFlagService: featureFlagService, + abTestVariationProvider: mockABTestVariationProvider) // When let isEligible = checker.isEligible() From b03230a106f5aa93bf5aee21d3d2d6d481e9ec63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Wed, 19 Jul 2023 12:14:29 +0200 Subject: [PATCH 34/41] Track go to coupons button tapped. --- WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift | 4 ++++ WooCommerce/Classes/Analytics/WooAnalyticsStat.swift | 1 + .../Orders/Order Creation/EditableOrderViewModel.swift | 6 ++++++ .../Order Creation/PaymentSection/OrderPaymentSection.swift | 1 + 4 files changed, 12 insertions(+) diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift index c9d627e4a03..d5fa7d06cf2 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift @@ -604,6 +604,10 @@ extension WooAnalyticsEvent { WooAnalyticsEvent(statName: .orderCouponRemove, properties: [Keys.flow: flow.rawValue]) } + static func orderGoToCouponsButtonTapped() -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .orderGoToCouponsButtonTapped, properties: [:]) + } + static func productDiscountAdd(type: FeeOrDiscountLineDetailsViewModel.FeeOrDiscountType) -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .orderProductDiscountAdd, properties: [Keys.type: type.rawValue]) } diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index 6ad7532e44c..4d2423c7cfb 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -407,6 +407,7 @@ public enum WooAnalyticsStat: String { case orderFeeRemove = "order_fee_remove" case orderCouponAdd = "order_coupon_add" case orderCouponRemove = "order_coupon_remove" + case orderGoToCouponsButtonTapped = "order_go_to_coupons_button_tapped" case orderProductDiscountAdd = "order_product_discount_add" case orderProductDiscountRemove = "order_product_discount_remove" case orderProductDiscountAddButtonTapped = "order_product_discount_add_button_tapped" diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift index cb7802cc673..8f0f76943e5 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift @@ -699,6 +699,7 @@ extension EditableOrderViewModel { let shippingLineViewModel: ShippingLineDetailsViewModel let feeLineViewModel: FeeOrDiscountLineDetailsViewModel let addNewCouponLineClosure: (Coupon) -> Void + let onGoToCouponsClosure: () -> Void init(siteID: Int64 = 0, itemsTotal: String = "0", @@ -723,6 +724,7 @@ extension EditableOrderViewModel { saveShippingLineClosure: @escaping (ShippingLine?) -> Void = { _ in }, saveFeeLineClosure: @escaping (String?) -> Void = { _ in }, addNewCouponLineClosure: @escaping (Coupon) -> Void = { _ in }, + onGoToCouponsClosure: @escaping () -> Void = {}, currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) { self.siteID = siteID self.itemsTotal = currencyFormatter.formatAmount(itemsTotal) ?? "0.00" @@ -754,6 +756,7 @@ extension EditableOrderViewModel { lineType: .fee, didSelectSave: saveFeeLineClosure) self.addNewCouponLineClosure = addNewCouponLineClosure + self.onGoToCouponsClosure = onGoToCouponsClosure } } @@ -1070,6 +1073,9 @@ private extension EditableOrderViewModel { addNewCouponLineClosure: { [weak self] coupon in self?.saveCouponLine(result: .added(newCode: coupon.code)) }, + onGoToCouponsClosure: { [weak self] in + self?.analytics.track(event: WooAnalyticsEvent.Orders.orderGoToCouponsButtonTapped()) + }, currencyFormatter: self.currencyFormatter) } .assign(to: &$paymentDataViewModel) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 425edc3d95f..dd82290fd82 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -97,6 +97,7 @@ struct OrderPaymentSection: View { Alert(title: Text(Localization.goToCoupons), message: Text(Localization.goToCouponsAlertMessage), primaryButton: .default(Text(Localization.goToCouponsAlertButtonTitle), action: { + viewModel.onGoToCouponsClosure() MainTabBarController.presentCoupons() }), secondaryButton: .cancel()) From f1e069394b7d7780549ac3e576cd6598e19d083b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Wed, 19 Jul 2023 12:21:07 +0200 Subject: [PATCH 35/41] Add unit test for tracking the go to coupons event --- .../Order Creation/EditableOrderViewModelTests.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/EditableOrderViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/EditableOrderViewModelTests.swift index e88a9376112..8d8f2cfb0ff 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/EditableOrderViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/EditableOrderViewModelTests.swift @@ -499,6 +499,18 @@ final class EditableOrderViewModelTests: XCTestCase { XCTAssertEqual(viewModel.paymentDataViewModel.orderTotal, "£0.00") } + func test_payment_data_view_model_when_calling_onGoToCouponsClosure_then_calls_to_track_event() { + // Given + let analytics = MockAnalyticsProvider() + + // When + let viewModel = EditableOrderViewModel(siteID: sampleSiteID, analytics: WooAnalytics(analyticsProvider: analytics)) + viewModel.paymentDataViewModel.onGoToCouponsClosure() + + // Then + XCTAssertEqual(analytics.receivedEvents.first, WooAnalyticsStat.orderGoToCouponsButtonTapped.rawValue) + } + // MARK: - Add Products to Order via SKU Scanner Tests func test_trackBarcodeScanningButtonTapped_tracks_right_event() { From b82415fa023af931ba9e53cf640a5e20df8d0905 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 19 Jul 2023 16:24:42 +0530 Subject: [PATCH 36/41] Remove feature flag and update unit tests. --- .../DefaultFeatureFlagService.swift | 2 -- Experiments/Experiments/FeatureFlag.swift | 4 ---- ...ddProductFromImageEligibilityChecker.swift | 8 ------- .../Mocks/MockFeatureFlagService.swift | 7 +----- ...ductFromImageEligibilityCheckerTests.swift | 24 ------------------- 5 files changed, 1 insertion(+), 44 deletions(-) diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index f858d7d1d7d..b31e742c087 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -97,8 +97,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return true case .freeTrialInAppPurchasesUpgradeM2: return buildConfig == .localDeveloper || buildConfig == .alpha - case .addProductFromImage: - return (buildConfig == .localDeveloper || buildConfig == .alpha) && !isUITesting case .ordersWithCouponsM4: return true default: diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 2218dae429c..439e651bc50 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -208,10 +208,6 @@ public enum FeatureFlag: Int { /// case freeTrialInAppPurchasesUpgradeM2 - /// A new flow to add product from an image. - /// - case addProductFromImage - /// Enables the Milestone 4 of the Orders with Coupons project: Adding discounts to products case ordersWithCouponsM4 } diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift index 022099f9b4d..567108b734f 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityChecker.swift @@ -1,6 +1,5 @@ import Foundation import Yosemite -import protocol Experiments.FeatureFlagService import Experiments /// Protocol for checking "add product from image" eligibility for easier unit testing. @@ -15,14 +14,11 @@ protocol AddProductFromImageEligibilityCheckerProtocol { /// Checks the eligibility for the "add product from image" feature. final class AddProductFromImageEligibilityChecker: AddProductFromImageEligibilityCheckerProtocol { private let stores: StoresManager - private let featureFlagService: FeatureFlagService private let abTestVariationProvider: ABTestVariationProvider init(stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, abTestVariationProvider: ABTestVariationProvider = DefaultABTestVariationProvider()) { self.stores = stores - self.featureFlagService = featureFlagService self.abTestVariationProvider = abTestVariationProvider } @@ -38,10 +34,6 @@ final class AddProductFromImageEligibilityChecker: AddProductFromImageEligibilit return false } - guard featureFlagService.isFeatureFlagEnabled(.addProductFromImage) else { - return false - } - return abTestVariationProvider.variation(for: .addProductFromImage) == .treatment } } diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index e979b5c3b4e..11de78050ab 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -27,7 +27,6 @@ struct MockFeatureFlagService: FeatureFlagService { private let isShareProductAIEnabled: Bool private let isJustInTimeMessagesOnDashboardEnabled: Bool private let isFreeTrialInAppPurchasesUpgradeM2: Bool - private let isAddProductFromImageEnabled: Bool init(isInboxOn: Bool = false, isSplitViewInOrdersTabOn: Bool = false, @@ -53,8 +52,7 @@ struct MockFeatureFlagService: FeatureFlagService { isBlazeEnabled: Bool = false, isShareProductAIEnabled: Bool = false, isJustInTimeMessagesOnDashboardEnabled: Bool = false, - isFreeTrialInAppPurchasesUpgradeM2: Bool = false, - isAddProductFromImageEnabled: Bool = false) { + isFreeTrialInAppPurchasesUpgradeM2: Bool = false) { self.isInboxOn = isInboxOn self.isSplitViewInOrdersTabOn = isSplitViewInOrdersTabOn self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn @@ -80,7 +78,6 @@ struct MockFeatureFlagService: FeatureFlagService { self.isBlazeEnabled = isBlazeEnabled self.isShareProductAIEnabled = isShareProductAIEnabled self.isJustInTimeMessagesOnDashboardEnabled = isJustInTimeMessagesOnDashboardEnabled - self.isAddProductFromImageEnabled = isAddProductFromImageEnabled } func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool { @@ -133,8 +130,6 @@ struct MockFeatureFlagService: FeatureFlagService { return isJustInTimeMessagesOnDashboardEnabled case .freeTrialInAppPurchasesUpgradeM2: return isFreeTrialInAppPurchasesUpgradeM2 - case .addProductFromImage: - return isAddProductFromImageEnabled default: return false } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift index c8a9e75e9d6..65615d4e6e7 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductFromImage/AddProductFromImageEligibilityCheckerTests.swift @@ -51,9 +51,7 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { mockABTestVariationProvider.mockVariationValue = .treatment updateDefaultStore(isWPCOMStore: true) - let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) let checker = AddProductFromImageEligibilityChecker(stores: stores, - featureFlagService: featureFlagService, abTestVariationProvider: mockABTestVariationProvider) // When @@ -69,27 +67,7 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { mockABTestVariationProvider.mockVariationValue = .treatment updateDefaultStore(isWPCOMStore: false) - let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) let checker = AddProductFromImageEligibilityChecker(stores: stores, - featureFlagService: featureFlagService, - abTestVariationProvider: mockABTestVariationProvider) - - // When - let isEligible = checker.isEligible() - - // Then - XCTAssertFalse(isEligible) - } - - func test_isEligible_is_false_for_wpcom_store_when_feature_flag_is_disabled() throws { - // Given - let mockABTestVariationProvider = MockABTestVariationProvider() - mockABTestVariationProvider.mockVariationValue = .treatment - - updateDefaultStore(isWPCOMStore: true) - let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: false) - let checker = AddProductFromImageEligibilityChecker(stores: stores, - featureFlagService: featureFlagService, abTestVariationProvider: mockABTestVariationProvider) // When @@ -105,9 +83,7 @@ final class AddProductFromImageEligibilityCheckerTests: XCTestCase { mockABTestVariationProvider.mockVariationValue = .control updateDefaultStore(isWPCOMStore: true) - let featureFlagService = MockFeatureFlagService(isAddProductFromImageEnabled: true) let checker = AddProductFromImageEligibilityChecker(stores: stores, - featureFlagService: featureFlagService, abTestVariationProvider: mockABTestVariationProvider) // When From 54062afeb822da5e1fc2ee2680548144cc6e2d41 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Wed, 19 Jul 2023 16:25:31 +0530 Subject: [PATCH 37/41] Add release notes. --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 6030ccd265c..a6adc2a8b2b 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ - [Internal] Media picker flow was refactored to support interactive dismissal for device photo picker and WordPress media picker sources. Affected flows: product form > images, and virtual product form > downloadable files. [https://github.com/woocommerce/woocommerce-ios/pull/10236] - [Internal] Orders: Improved error message when orders can't be loaded due to a parsing (decoding) error. [https://github.com/woocommerce/woocommerce-ios/pull/10252] - [**] Product discounts: Users can now add discounts to products when creating an order. [https://github.com/woocommerce/woocommerce-ios/pull/10244] +- [Internal] A new way to create a product from an image using AI is being A/B tested. [https://github.com/woocommerce/woocommerce-ios/pull/10253] 14.5 From 6dbb5b05e1b16f9916ad5641cadf99822b98be11 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Jul 2023 16:27:56 +0200 Subject: [PATCH 38/41] Add extra padding between disclaimer and footer --- .../Classes/ViewRelated/Upgrades/FullFeatureListView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift index a9a58157ea9..0e1ff184355 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift @@ -51,6 +51,7 @@ struct FullFeatureListView: View { .font(.caption) } .background(Color(.secondarySystemBackground)) + .padding(.top) } .padding() .navigationTitle(Localization.featureListTitleText) From 01eb013313b3412557a64a540790dd9535d36938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Vargas=20Casaseca?= Date: Wed, 19 Jul 2023 16:35:59 +0200 Subject: [PATCH 39/41] Fix typo Co-authored-by: Gabriel Maldonado --- .../Order Creation/PaymentSection/OrderPaymentSection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift index 425edc3d95f..9a79c918693 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift @@ -176,7 +176,7 @@ private extension OrderPaymentSection { "when creating a new order that navigates to the Coupons Section") static let goToCouponsAlertMessage = NSLocalizedString("Do you want to navigate to the Coupons Menu? These changes will be discarded.", comment: "Confirm message for navigating to coupons when creating a new order") - static let goToCouponsAlertButtonTitle = NSLocalizedString("Go", comment: "Confirm buton titele for navigating to coupons when creating a new order") + static let goToCouponsAlertButtonTitle = NSLocalizedString("Go", comment: "Confirm button title for navigating to coupons when creating a new order") static let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title when showing the coupon list selector") } } From 5b7345a9461f4f3eab55c7e89aa7244f262d5586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Wed, 19 Jul 2023 16:43:53 +0200 Subject: [PATCH 40/41] Cleaner code --- WooCommerce/Classes/ViewRelated/MainTabBarController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 4ab6d5a8bac..ce8fd86b18d 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -195,7 +195,7 @@ final class MainTabBarController: UITabBarController { /// - animated: Whether the tab switch is animated. /// - completion: Invoked when switching to the tab's root screen is complete. func navigateToTabWithNavigationController(_ tab: WooTab, animated: Bool = false, completion: ((UINavigationController) -> Void)? = nil) { - view.window?.rootViewController?.dismiss(animated: true, completion: nil) + dismiss(animated: true, completion: nil) selectedIndex = tab.visibleIndex() if let navController = selectedViewController as? UINavigationController { From a09d032ad7c0b147b2f8bb694f7bbfc2879db47e Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Jul 2023 16:51:44 +0200 Subject: [PATCH 41/41] Update comment strings for common static let --- .../Upgrades/FullFeatureListViewModel.swift | 99 ++++++++++--------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift index 298988df25c..204044fc62f 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift @@ -83,192 +83,197 @@ struct FullFeatureListViewModel { } private extension FullFeatureListViewModel { + static let featureGroupTitleComment = "The title of one of the feature groups offered with paid plans" + static let essentialFeatureTitleComment = "The title of one of the features offered with the Essential plan" + static let performanceFeatureTitleComment = "The title of one of the features offered with the Performance plan" + struct Localization { static let yourStoreFeatureTitle = NSLocalizedString( "Your Store", - comment: "The title of one of the feature groups offered with paid plans") + comment: featureGroupTitleComment) static let productsFeatureTitle = NSLocalizedString( "Products", - comment: "The title of one of the feature groups offered with paid plans") + comment: featureGroupTitleComment) static let paymentsFeatureTitle = NSLocalizedString( "Payments", - comment: "The title of one of the feature groups offered with paid plans") + comment: featureGroupTitleComment) static let marketingAndEmailFeatureTitle = NSLocalizedString( "Marketing & Email", - comment: "The title of one of the feature groups offered with paid plans") + comment: featureGroupTitleComment) static let shippingFeatureTitle = NSLocalizedString( "Shipping", - comment: "The title of one of the feature groups offered with paid plans") + comment: featureGroupTitleComment) static let wooCommerceStoreText = NSLocalizedString( "WooCommerce store", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let wooCommerceMobileAppText = NSLocalizedString( "WooCommerce mobile app", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let wordPressCMSText = NSLocalizedString( "WordPress CMS", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let wordPressMobileAppText = NSLocalizedString( "WordPress mobile app", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let freeSSLCertificateText = NSLocalizedString( "Free SSL certificate", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let generousStorageText = NSLocalizedString( "Generous storage", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let automatedBackupQuickRestoreText = NSLocalizedString( "Automated backup + quick restore", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let adFreeExperienceText = NSLocalizedString( "Ad-free experience", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let unlimitedAdminAccountsText = NSLocalizedString( "Unlimited admin accounts", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let liveChatSupportText = NSLocalizedString( "Live chat support", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let emailSupportText = NSLocalizedString( "Email support", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let premiumThemesIncludedText = NSLocalizedString( "Premium themes included", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let salesReportsText = NSLocalizedString( "Sales reports", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let googleAnalyticsText = NSLocalizedString( "Google Analytics", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let listUnlimitedProducts = NSLocalizedString( "List unlimited products", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let giftCards = NSLocalizedString( "Gift cards", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let listProductsByBrand = NSLocalizedString( "List products by brand", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let integratedPayments = NSLocalizedString( "Integrated payments", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let internationalPayments = NSLocalizedString( "International payments'", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let automatedSalesTaxes = NSLocalizedString( "Automated sales taxes", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let acceptLocalPayments = NSLocalizedString( "Accept local payments'", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let recurringPayments = NSLocalizedString( "Recurring payments'", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) + static let promoteOnTikTok = NSLocalizedString( "Promote on TikTok", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let syncWithPinterest = NSLocalizedString( "Sync with Pinterest", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let connectWithFacebook = NSLocalizedString( "Connect with Facebook", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let advancedSeoTools = NSLocalizedString( "Advanced SEO tools", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let advertiseOnGoogle = NSLocalizedString( "Advertise on Google", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let customOrderEmails = NSLocalizedString( "Custom order emails", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let shipmentTracking = NSLocalizedString( "Shipment tracking", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let liveShippingRates = NSLocalizedString( "Live shipping rates", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let printShippingLabels = NSLocalizedString( "Print shipping labels²", - comment: "The title of one of the features offered with the Essential plan") + comment: essentialFeatureTitleComment) static let minMaxOrderQuantityText = NSLocalizedString( "Min/Max order quantity", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let productBundlesText = NSLocalizedString( "Product Bundles", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let customProductKitsText = NSLocalizedString( "Custom product kits", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let productRecommendationsText = NSLocalizedString( "Product recommendations", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let backInStockEmailsText = NSLocalizedString( "Back in stock emails", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let marketingAutomationText = NSLocalizedString( "Marketing automation", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let abandonedCartRecoveryText = NSLocalizedString( "Abandoned cart recovery", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let referralProgramsText = NSLocalizedString( "Referral programs", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let customerBirthdayEmailsText = NSLocalizedString( "Customer birthday emails", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let loyaltyPointsProgramsText = NSLocalizedString( "Loyalty points programs", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) static let discountedShippingText = NSLocalizedString( "Discounted shipping²", - comment: "The title of one of the features offered with the Performance plan") + comment: performanceFeatureTitleComment) } }