diff --git a/Modules/Sources/Networking/Mapper/InAppPurchaseOrderResultMapper.swift b/Modules/Sources/Networking/Mapper/InAppPurchaseOrderResultMapper.swift deleted file mode 100644 index 9f55e4e7f70..00000000000 --- a/Modules/Sources/Networking/Mapper/InAppPurchaseOrderResultMapper.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - - -/// Mapper: IAP Order Creation Result -/// -struct InAppPurchaseOrderResultMapper: Mapper { - - /// (Attempts) to extract the order ID from a given JSON Encoded response. - /// - func map(response: Data) throws -> Int { - - let dictionary = try JSONDecoder().decode([String: AnyDecodable].self, from: response) - guard let orderId = (dictionary[Constants.orderIdKey]?.value as? Int) else { - throw Error.parseError - } - return orderId - } -} - -private extension InAppPurchaseOrderResultMapper { - enum Constants { - static let orderIdKey: String = "order_id" - } - - enum Error: Swift.Error { - case parseError - } -} diff --git a/Modules/Sources/Networking/Mapper/InAppPurchasesProductMapper.swift b/Modules/Sources/Networking/Mapper/InAppPurchasesProductMapper.swift deleted file mode 100644 index 716b83200ad..00000000000 --- a/Modules/Sources/Networking/Mapper/InAppPurchasesProductMapper.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// Mapper: IAP Product -/// -struct InAppPurchasesProductMapper: Mapper { - /// (Attempts) to convert a dictionary into a list of product identifiers. - /// - func map(response: Data) throws -> [String] { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) - return try decoder.decode([String].self, from: response) - } -} diff --git a/Modules/Sources/Networking/Mapper/InAppPurchasesTransactionMapper.swift b/Modules/Sources/Networking/Mapper/InAppPurchasesTransactionMapper.swift deleted file mode 100644 index e76a1b019ca..00000000000 --- a/Modules/Sources/Networking/Mapper/InAppPurchasesTransactionMapper.swift +++ /dev/null @@ -1,34 +0,0 @@ - -import Foundation - -/// Mapper: IAP-WPCOM transaction verification result -/// -struct InAppPurchasesTransactionMapper: Mapper { - func map(response: Data) throws -> InAppPurchasesTransactionResponse { - let decoder = JSONDecoder() - return try decoder.decode(InAppPurchasesTransactionResponse.self, from: response) - } -} - -public struct InAppPurchasesTransactionResponse: Decodable { - public let siteID: Int64? - public let message: String? - public let code: Int? - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let siteID = try container.decodeIfPresent(Int64.self, forKey: .siteID) - let errorMessage = try container.decodeIfPresent(String.self, forKey: .message) - let errorCode = try container.decodeIfPresent(Int.self, forKey: .code) - self.siteID = siteID - self.message = errorMessage - self.code = errorCode - } - - private enum CodingKeys: String, CodingKey { - case siteID = "site_id" - case message - case code - } -} diff --git a/Modules/Sources/Networking/Model/Site.swift b/Modules/Sources/Networking/Model/Site.swift index a99d1afe5b0..d1aee932299 100644 --- a/Modules/Sources/Networking/Model/Site.swift +++ b/Modules/Sources/Networking/Model/Site.swift @@ -265,12 +265,6 @@ public extension Site { var isSimpleSite: Bool { plan == WooConstants.freePlanSlug } - - /// Whether the site is running a free trial WooExpress plan - /// - var isFreeTrialSite: Bool { - plan == WooConstants.freeTrialPlanSlug - } } /// Defines all of the Site CodingKeys. diff --git a/Modules/Sources/Networking/Remote/FeatureFlagRemote.swift b/Modules/Sources/Networking/Remote/FeatureFlagRemote.swift index 529cae6a297..7e64fa2c26b 100644 --- a/Modules/Sources/Networking/Remote/FeatureFlagRemote.swift +++ b/Modules/Sources/Networking/Remote/FeatureFlagRemote.swift @@ -28,7 +28,6 @@ public class FeatureFlagRemote: Remote, FeatureFlagRemoteProtocol { public enum RemoteFeatureFlag: Decodable { case storeCreationCompleteNotification - case hardcodedPlanUpgradeDetailsMilestone1AreAccurate case pointOfSale case appPasswordsForJetpackSites @@ -36,8 +35,6 @@ public enum RemoteFeatureFlag: Decodable { switch rawValue { case "woo_notification_store_creation_ready": self = .storeCreationCompleteNotification - case "woo_hardcoded_plan_upgrade_details_milestone_1_are_accurate": - self = .hardcodedPlanUpgradeDetailsMilestone1AreAccurate case "woo_pos": self = .pointOfSale case "woo_app_passwords_for_jetpack_sites": diff --git a/Modules/Sources/Networking/Remote/InAppPurchasesRemote.swift b/Modules/Sources/Networking/Remote/InAppPurchasesRemote.swift deleted file mode 100644 index 8bca8e7026c..00000000000 --- a/Modules/Sources/Networking/Remote/InAppPurchasesRemote.swift +++ /dev/null @@ -1,169 +0,0 @@ -import Alamofire -import Foundation - -/// In-app Purchases Endpoints -/// -public class InAppPurchasesRemote: Remote { - public typealias InAppPurchasesTransactionResult = Swift.Result - /// Retrieves a list of product identifiers available for purchase - /// - /// - Parameters: - /// - completion: Closure to be executed upon completion - /// - public func loadProducts(completion: @escaping (Swift.Result<[String], Error>) -> Void) { - let request = DotcomRequest(wordpressApiVersion: .wpcomMark2, method: .get, path: Constants.productsPath, headers: headersWithAppId) - let mapper = InAppPurchasesProductMapper() - enqueue(request, mapper: mapper, completion: completion) - } - - /// Checks the WPCOM billing system for whether or not an In-app Purchase transaction has been handled - /// - /// Handled transactions are those which can be found in the WPCOM billing system. Unhandled transactions are those which couldn't be found. - /// We return the Site ID associated with the handled transaction as part of the InAppPurchasesTransactionResponse, or a "transaction not found" - /// response if has not been handled yet. - /// - /// - Parameters: - /// - transactionID: The transactionID of the specific transaction (not originalTransactionID) - /// - completion: Closure to be executed upon completion. - /// - public func retrieveHandledTransactionResult(for transactionID: UInt64, - completion: @escaping (InAppPurchasesTransactionResult) -> Void ) { - let request = DotcomRequest(wordpressApiVersion: .wpcomMark2, - method: .get, - path: Constants.transactionsPath + "/\(transactionID)", - headers: headersWithAppId) - let mapper = InAppPurchasesTransactionMapper() - enqueue(request, mapper: mapper, completion: completion) - } - - /// Creates a new order for a new In-app Purchase - /// - Parameters: - /// - siteID: Site the purchase is for - /// - price: An integer representation of the price in cents (5.99 -> 599) - /// - productIdentifier: The IAP sku for the product - /// - appStoreCountryCode: The country of the user's App Store - /// - originalTransactionId: The original transaction id of the transaction - /// - completion: Closure to be executed upon completion - /// - public func createOrder( - for siteID: Int64, - price: Int, - productIdentifier: String, - appStoreCountryCode: String, - originalTransactionId: UInt64, - transactionId: UInt64, - subscriptionGroupId: String?, - completion: @escaping (Swift.Result) -> Void) { - var parameters: [String: Any] = [ - Constants.siteIDKey: siteID, - Constants.priceKey: price, - Constants.productIDKey: productIdentifier, - Constants.appStoreCountryCodeKey: appStoreCountryCode, - Constants.originalTransactionIdKey: originalTransactionId, - Constants.transactionIdKey: transactionId - ] - if let subscriptionGroupId { - parameters[Constants.subscriptionGroupIdKey] = subscriptionGroupId - } - let request = DotcomRequest( - wordpressApiVersion: .wpcomMark2, - method: .post, - path: Constants.ordersPath, - parameters: parameters, - headers: headersWithAppId - ) - let mapper = InAppPurchaseOrderResultMapper() - enqueue(request, mapper: mapper, completion: completion) - } -} - -// MARK: - Async methods - -public extension InAppPurchasesRemote { - /// Retrieves a list of product identifiers available for purchase - /// - /// - Returns: a list of product identifiers. - /// - func loadProducts() async throws -> [String] { - try await withCheckedThrowingContinuation { continuation in - loadProducts { result in - continuation.resume(with: result) - } - } - } - - /// Checks the WPCOM billing system for whether or not an In-app Purchase transaction has been handled - /// - ///- Returns: A InAppPurchasesTransactionResponse, which will contain the siteID the transactionID belongs to for handled transactions, - /// or a "transaction not found" response for unhandled transactions - /// - func retrieveHandledTransactionResult(for transactionID: UInt64) async throws -> InAppPurchasesTransactionResponse { - try await withCheckedThrowingContinuation { continuation in - retrieveHandledTransactionResult(for: transactionID) { result in - continuation.resume(with: result) - } - } - } - - /// Creates a new order for a new In-app Purchase - /// - Parameters: - /// - siteID: Site the purchase is for - /// - price: An integer representation of the price in cents (5.99 -> 599) - /// - productIdentifier: The IAP sku for the product - /// - appStoreCountryCode: The country of the user's App Store - /// - originalTransactionId: The original transaction id of the transaction - /// - /// - Returns: The ID of the created order. - /// - func createOrder( - for siteID: Int64, - price: Int, - productIdentifier: String, - appStoreCountryCode: String, - originalTransactionId: UInt64, - transactionId: UInt64, - subscriptionGroupId: String? - ) async throws -> Int { - try await withCheckedThrowingContinuation { continuation in - createOrder( - for: siteID, - price: price, - productIdentifier: productIdentifier, - appStoreCountryCode: appStoreCountryCode, - originalTransactionId: originalTransactionId, - transactionId: transactionId, - subscriptionGroupId: subscriptionGroupId - ) { result in - continuation.resume(with: result) - } - } - } -} - -private extension InAppPurchasesRemote { - var headersWithAppId: [String: String]? { - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { - return nil - } - - return [ - "X-APP-ID": bundleIdentifier - ] - } -} - -private extension InAppPurchasesRemote { - enum Constants { - static let productsPath = "iap/products" - static let ordersPath = "iap/orders" - static let transactionsPath = "iap/transactions" - - static let siteIDKey = "site_id" - static let priceKey = "price" - static let productIDKey = "product_id" - static let appStoreCountryCodeKey = "appstore_country" - static let originalTransactionIdKey = "original_transaction_id" - static let transactionIdKey = "transaction_id" - static let subscriptionGroupIdKey = "subscription_group_id" - } -} diff --git a/Modules/Sources/Networking/Remote/PaymentRemote.swift b/Modules/Sources/Networking/Remote/PaymentRemote.swift index 8858b22f53e..8efdb35a5a8 100644 --- a/Modules/Sources/Networking/Remote/PaymentRemote.swift +++ b/Modules/Sources/Networking/Remote/PaymentRemote.swift @@ -28,6 +28,7 @@ public protocol PaymentRemoteProtocol { /// WPCOM Payment Endpoints /// +// periphery:ignore public class PaymentRemote: Remote, PaymentRemoteProtocol { public func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan { let path = Path.products @@ -103,6 +104,7 @@ public struct WPComPlan: Decodable, Equatable { } /// Contains necessary data for a site's WPCOM plan. +// periphery:ignore public struct WPComSitePlan: Equatable { /// ID of the WPCOM plan. /// diff --git a/Modules/Sources/NetworkingCore/Settings/WooConstants.swift b/Modules/Sources/NetworkingCore/Settings/WooConstants.swift index 19023b56745..6f8957e9e16 100644 --- a/Modules/Sources/NetworkingCore/Settings/WooConstants.swift +++ b/Modules/Sources/NetworkingCore/Settings/WooConstants.swift @@ -13,6 +13,4 @@ public enum WooConstants { /// Slug of the free plan public static let freePlanSlug = "free_plan" - /// Slug of the free trial WooExpress plan - public static let freeTrialPlanSlug = "ecommerce-trial-bundle-monthly" } diff --git a/Modules/Sources/WooFoundationCore/Analytics/WooAnalyticsStat.swift b/Modules/Sources/WooFoundationCore/Analytics/WooAnalyticsStat.swift index d446839a965..e1f99108ffb 100644 --- a/Modules/Sources/WooFoundationCore/Analytics/WooAnalyticsStat.swift +++ b/Modules/Sources/WooFoundationCore/Analytics/WooAnalyticsStat.swift @@ -1170,16 +1170,6 @@ public enum WooAnalyticsStat: String { case planUpgradeSuccess = "plan_upgrade_success" case planUpgradeAbandoned = "plan_upgrade_abandoned" - // MARK: In-App Purchases - case planUpgradePurchaseButtonTapped = "plan_upgrade_purchase_button_tapped" - case planUpgradeScreenLoaded = "plan_upgrade_screen_loaded" - case planUpgradeScreenDismissed = "plan_upgrade_screen_dismissed" - case planUpgradeDetailsScreenLoaded = "plan_upgrade_details_screen_loaded" - case planUpgradeProcessingScreenLoaded = "plan_upgrade_processing_screen_loaded" - case planUpgradeCompletedScreenLoaded = "plan_upgrade_completed_screen_loaded" - case planUpgradePurchaseFailed = "plan_upgrade_purchase_failed" - - // MARK: Application password authorization in web view case applicationPasswordAuthorizationButtonTapped = "application_password_authorization_button_tapped" case applicationPasswordAuthorizationWebViewShown = "application_password_authorization_web_view_shown" diff --git a/Modules/Sources/Yosemite/Actions/InAppPurchaseAction.swift b/Modules/Sources/Yosemite/Actions/InAppPurchaseAction.swift deleted file mode 100644 index 726c95a7c37..00000000000 --- a/Modules/Sources/Yosemite/Actions/InAppPurchaseAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import StoreKit - -public enum InAppPurchaseAction: Action { - case loadProducts(completion: (Result<[StoreKit.Product], Error>) -> Void) - case purchaseProduct(siteID: Int64, productID: String, completion: (Result) -> Void) - case userIsEntitledToProduct(productID: String, completion: (Result) -> Void) - case inAppPurchasesAreSupported(completion: (Bool) -> Void) - case retryWPComSyncForPurchasedProduct(productID: String, completion: (Result<(), Error>) -> Void) - case siteHasCurrentInAppPurchases(siteID: Int64, completion: (Bool) -> Void) -} diff --git a/Modules/Sources/Yosemite/Model/WooPlans/WooPlan.swift b/Modules/Sources/Yosemite/Model/WooPlans/WooPlan.swift deleted file mode 100644 index a2e48bea479..00000000000 --- a/Modules/Sources/Yosemite/Model/WooPlans/WooPlan.swift +++ /dev/null @@ -1,311 +0,0 @@ -import Foundation - -public struct WooPlan: Decodable, Identifiable { - public let id: String - public let name: String - public let shortName: String - public let planFrequency: PlanFrequency - public let planDescription: String - public let planFeatures: [String] - - public init(id: String, - name: String, - shortName: String, - planFrequency: PlanFrequency, - planDescription: String, - planFeatures: [String]) { - self.id = id - self.name = name - self.shortName = shortName - self.planFrequency = planFrequency - self.planDescription = planDescription - self.planFeatures = planFeatures - } - - public var isEssential: Bool { - Self.isEssential(id) - } - - public static func isEssential(_ plan: String) -> Bool { - let essentialMonthly = AvailableInAppPurchasesWPComPlans.essentialMonthly.rawValue - let essentialYearly = AvailableInAppPurchasesWPComPlans.essentialYearly.rawValue - - if plan == essentialMonthly || plan == essentialYearly { - return true - } else { - return false - } - } - - static func isYearly(_ planID: String) -> Bool { - guard let plan = AvailableInAppPurchasesWPComPlans(rawValue: planID) else { - return false - } - - switch plan { - case .essentialYearly, .performanceYearly: - return true - case .essentialMonthly, .performanceMonthly: - return false - } - } - - public static func loadM2HardcodedPlans() -> [WooPlan] { - [WooPlan(id: AvailableInAppPurchasesWPComPlans.essentialMonthly.rawValue, - name: Localization.essentialPlanName(frequency: .month), - shortName: Localization.essentialPlanShortName, - planFrequency: .month, - planDescription: Localization.essentialPlanDescription, - planFeatures: loadHardcodedPlanFeatures(AvailableInAppPurchasesWPComPlans.essentialMonthly.rawValue)), - WooPlan(id: AvailableInAppPurchasesWPComPlans.essentialYearly.rawValue, - name: Localization.essentialPlanName(frequency: .year), - shortName: Localization.essentialPlanShortName, - planFrequency: .year, - planDescription: Localization.essentialPlanDescription, - planFeatures: loadHardcodedPlanFeatures(AvailableInAppPurchasesWPComPlans.essentialYearly.rawValue)), - WooPlan(id: AvailableInAppPurchasesWPComPlans.performanceMonthly.rawValue, - name: Localization.performancePlanName(frequency: .month), - shortName: Localization.performancePlanShortName, - planFrequency: .month, - planDescription: Localization.performancePlanDescription, - planFeatures: loadHardcodedPlanFeatures(AvailableInAppPurchasesWPComPlans.performanceMonthly.rawValue)), - WooPlan(id: AvailableInAppPurchasesWPComPlans.performanceYearly.rawValue, - name: Localization.performancePlanName(frequency: .year), - shortName: Localization.performancePlanShortName, - planFrequency: .year, - planDescription: Localization.performancePlanDescription, - planFeatures: loadHardcodedPlanFeatures(AvailableInAppPurchasesWPComPlans.performanceYearly.rawValue))] - } - - private static func loadHardcodedPlanFeatures(_ planID: String) -> [String] { - if isEssential(planID) { - var planFeatures = isYearly(planID) ? [Localization.freeCustomDomainFeatureText] : [] - planFeatures += [ - Localization.supportFeatureText, - Localization.unlimitedAdminsFeatureText, - Localization.unlimitedProductsFeatureText, - Localization.premiumThemesFeatureText, - Localization.internationalSalesFeatureText, - Localization.autoSalesTaxFeatureText, - Localization.autoBackupsFeatureText, - Localization.integratedShipmentFeatureText, - Localization.analyticsDashboardFeatureText, - Localization.giftVouchersFeatureText, - Localization.emailMarketingFeatureText, - Localization.marketplaceSyncFeatureText, - Localization.advancedSEOFeatureText, - ] - return planFeatures - } else { - return [ - Localization.stockNotificationsFeatureText, - Localization.marketingAutomationFeatureText, - Localization.emailTriggersFeatureText, - Localization.cartAbandonmentEmailsFeatureText, - Localization.referralProgramsFeatureText, - Localization.birthdayEmailsFeatureText, - Localization.loyaltyProgramsFeatureText, - Localization.bulkDiscountsFeatureText, - Localization.addOnsFeatureText, - Localization.assembledProductsFeatureText, - Localization.orderQuantityFeatureText - ] - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - id = try container.decode(String.self, forKey: .planId) - name = try container.decode(String.self, forKey: .planName) - shortName = try container.decode(String.self, forKey: .planShortName) - planFrequency = try container.decode(PlanFrequency.self, forKey: .planFrequency) - planDescription = try container.decode(String.self, forKey: .planDescription) - planFeatures = try container.decode([String].self, forKey: .planFeatures) - } - - private enum CodingKeys: String, CodingKey { - case planId = "plan_id" - case planName = "plan_name" - case planShortName = "plan_short_name" - case planFrequency = "plan_frequency" - case planDescription = "plan_description" - case planFeatures = "plan_features" - } - - public enum PlanFrequency: String, Decodable, Identifiable, CaseIterable { - case month - case year - - public var id: Self { - return self - } - - public var localizedString: String { - switch self { - case .month: - return Localization.month - case .year: - return Localization.year - } - } - - private enum Localization { - static let month = NSLocalizedString("per month", comment: "Description of the frequency of a monthly Woo plan") - static let year = NSLocalizedString("per year", comment: "Description of the frequency of a yearly Woo plan") - - static let monthPlanName = NSLocalizedString("Monthly", comment: "Description of the frequency of a monthly Woo plan for use in the plan name") - static let yearPlanName = NSLocalizedString("Yearly", comment: "Description of the frequency of a yearly Woo plan for use in the plan name") - } - - public var localizedPlanName: String { - switch self { - case .month: - return Localization.monthPlanName - case .year: - return Localization.yearPlanName - } - } - } -} - -public enum AvailableInAppPurchasesWPComPlans: String { - case essentialMonthly = "woocommerce.express.essential.monthly" - case essentialYearly = "woocommerce.express.essential.yearly" - case performanceMonthly = "woocommerce.express.performance.monthly" - case performanceYearly = "woocommerce.express.performance.yearly" -} - -private extension WooPlan { - enum Localization { - static let essentialPlanNameFormat = NSLocalizedString( - "Woo Express Essential %1$@", - comment: "Title for the plan on a screen showing the Woo Express Essential plan upgrade benefits. " + - "%1$@ will be replaced with Monthly or Yearly, and should be included in translations.") - static func essentialPlanName(frequency: PlanFrequency) -> String { - String.localizedStringWithFormat(essentialPlanNameFormat, frequency.localizedPlanName) - } - - static let essentialPlanShortName = NSLocalizedString( - "Essential", - comment: "Short name for the plan on a screen showing the Woo Express Essential plan upgrade benefits") - static let essentialPlanDescription = NSLocalizedString( - "Everything you need to set up your store and start selling your products.", - comment: "Description of the plan on a screen showing the Woo Express Essential plan upgrade benefits") - - static let performancePlanNameFormat = NSLocalizedString( - "Woo Express Performance %1$@", - comment: "Title for the plan on a screen showing the Woo Express Performance plan upgrade benefits " + - "%1$@ will be replaced with Monthly or Yearly, and should be included in translations") - static func performancePlanName(frequency: PlanFrequency) -> String { - String.localizedStringWithFormat(performancePlanNameFormat, frequency.localizedPlanName) - } - static let performancePlanShortName = NSLocalizedString( - "Performance", - comment: "Short name for the plan on a screen showing the Woo Express Performance plan upgrade benefits") - static let performancePlanDescription = NSLocalizedString( - "Accelerate your growth with advanced features.", - comment: "Description of the plan on a screen showing the Woo Express Performance plan upgrade benefits") - - /// Plan features - static let freeCustomDomainFeatureText = NSLocalizedString( - "Free custom domain for 1 year", - comment: "Description of one of the Essential plan features") - - static let supportFeatureText = NSLocalizedString( - "Support via live chat and email", - comment: "Description of one of the Essential plan features") - - static let unlimitedAdminsFeatureText = NSLocalizedString( - "Unlimited admin accounts", - comment: "Description of one of the Essential plan features") - - static let unlimitedProductsFeatureText = NSLocalizedString( - "Create unlimited products", - comment: "Description of one of the Essential plan features") - - static let premiumThemesFeatureText = NSLocalizedString( - "Premium themes", - comment: "Description of one of the Essential plan features") - - static let internationalSalesFeatureText = NSLocalizedString( - "Sell internationally", - comment: "Description of one of the Essential plan features") - - static let autoSalesTaxFeatureText = NSLocalizedString( - "Automatic sales tax", - comment: "Description of one of the Essential plan features") - - static let autoBackupsFeatureText = NSLocalizedString( - "Automated backups and security scans", - comment: "Description of one of the Essential plan features") - - static let integratedShipmentFeatureText = NSLocalizedString( - "Integrated shipment tracking", - comment: "Description of one of the Essential plan features") - - static let analyticsDashboardFeatureText = NSLocalizedString( - "Analytics dashboard", - comment: "Description of one of the Essential plan features") - - static let giftVouchersFeatureText = NSLocalizedString( - "Sell and accept e-gift vouchers", - comment: "Description of one of the Essential plan features") - - static let emailMarketingFeatureText = NSLocalizedString( - "Email marketing built-in", - comment: "Description of one of the Essential plan features") - - static let marketplaceSyncFeatureText = NSLocalizedString( - "Marketplace sync and social media integrations", - comment: "Description of one of the Essential plan features") - - static let advancedSEOFeatureText = NSLocalizedString( - "Advanced SEO tools", - comment: "Description of one of the Essential plan features") - - static let stockNotificationsFeatureText = NSLocalizedString( - "Back in stock notifications", - comment: "Description of one of the Performance plan features") - - static let marketingAutomationFeatureText = NSLocalizedString( - "Marketing automation", - comment: "Description of one of the Performance plan features") - - static let emailTriggersFeatureText = NSLocalizedString( - "Automated email triggers", - comment: "Description of one of the Performance plan features") - - static let cartAbandonmentEmailsFeatureText = NSLocalizedString( - "Cart abandonment emails", - comment: "Description of one of the Performance plan features") - - static let referralProgramsFeatureText = NSLocalizedString( - "Referral programs", - comment: "Description of one of the Performance plan features") - - static let birthdayEmailsFeatureText = NSLocalizedString( - "Customer birthday emails", - comment: "Description of one of the Performance plan features") - - static let loyaltyProgramsFeatureText = NSLocalizedString( - "Loyalty points programs", - comment: "Description of one of the Performance plan features") - - static let bulkDiscountsFeatureText = NSLocalizedString( - "Offer bulk discounts", - comment: "Description of one of the Performance plan features") - - static let addOnsFeatureText = NSLocalizedString( - "Recommend add-ons", - comment: "Description of one of the Performance plan features") - - static let assembledProductsFeatureText = NSLocalizedString( - "Assembled products and kits", - comment: "Description of one of the Performance plan features") - - static let orderQuantityFeatureText = NSLocalizedString( - "Minimum/maximum order quantity", - comment: "Description of one of the Performance plan features") - } -} diff --git a/Modules/Sources/Yosemite/Stores/InAppPurchaseStore.swift b/Modules/Sources/Yosemite/Stores/InAppPurchaseStore.swift deleted file mode 100644 index 6bfea1239d4..00000000000 --- a/Modules/Sources/Yosemite/Stores/InAppPurchaseStore.swift +++ /dev/null @@ -1,506 +0,0 @@ -import Combine -import Foundation -import Storage -import StoreKit -import Networking - -public class InAppPurchaseStore: Store { - public typealias PurchaseCompletionHandler = (Result) -> Void - // ISO 3166-1 Alpha-3 country code representation. - private let supportedCountriesCodes = ["USA"] - private var listenTask: Task? - private let remote: InAppPurchasesRemote - private var useBackend = true - private var pauseTransactionListener = CurrentValueSubject(false) - - /// When an IAP transaction requires further action, e.g. Strong Customer Auth in a banking app - /// or parental approval, it will be returned as a `.pending` transaction. - /// In those cases, we handle the transaction when it comes up in the `.updates` stream of - /// Transactions, which we listen to for handling unfinished transactions on app strat. - /// `pendingTransactionCompletionHandler`, holds the completion handler so that we - /// can update the original purchase flow, if it's still on screen. - /// N.B. Apple do not notify us about declined pending transactions, so we cannot handle them – - /// the user must dismiss the waiting screen and try again. - /// https://developer.apple.com/forums/thread/685183?answerId=682554022#682554022 - private var pendingTransactionCompletionHandler: PurchaseCompletionHandler? = nil - - public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) { - remote = InAppPurchasesRemote(network: network) - super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) - listenForTransactions() - } - - deinit { - listenTask?.cancel() - } - - public override func registerSupportedActions(in dispatcher: Dispatcher) { - dispatcher.register(processor: self, for: InAppPurchaseAction.self) - } - - public override func onAction(_ action: Action) { - guard let action = action as? InAppPurchaseAction else { - assertionFailure("InAppPurchaseStore received an unsupported action") - return - } - switch action { - case .loadProducts(let completion): - loadProducts(completion: completion) - case .purchaseProduct(let siteID, let productID, let completion): - purchaseProduct(siteID: siteID, productID: productID, completion: completion) - case .retryWPComSyncForPurchasedProduct(let productID, let completion): - Task { - do { - completion(.success(try await retryWPComSyncForPurchasedProduct(with: productID))) - } catch { - completion(.failure(error)) - } - } - case .inAppPurchasesAreSupported(completion: let completion): - Task { - completion(await inAppPurchasesAreSupported()) - } - case .userIsEntitledToProduct(productID: let productID, completion: let completion): - Task { - do { - completion(.success(try await userIsEntitledToProduct(with: productID))) - } catch { - completion(.failure(error)) - } - } - case .siteHasCurrentInAppPurchases(siteID: let siteID, completion: let completion): - Task { - completion(await siteHasCurrentInAppPurchases(siteID: siteID)) - } - } - } -} - -private extension InAppPurchaseStore { - func loadProducts(completion: @escaping (Result<[StoreKit.Product], Error>) -> Void) { - Task { - do { - try await assertInAppPurchasesAreSupported() - let identifiers = try await getProductIdentifiers() - logInfo("Requesting StoreKit products: \(identifiers)") - let products = try await StoreKit.Product.products(for: identifiers) - logInfo("Obtained product list from StoreKit: \(products.map({ $0.id }))") - completion(.success(products)) - } catch { - logError("Failed obtaining product list from StoreKit: \(error)") - completion(.failure(error)) - } - } - } - - func purchaseProduct(siteID: Int64, productID: String, completion: @escaping PurchaseCompletionHandler) { - Task { - do { - try await assertInAppPurchasesAreSupported() - - guard let product = try await StoreKit.Product.products(for: [productID]).first else { - return completion(.failure(Errors.transactionProductUnknown)) - } - - logInfo("Purchasing product \(product.id) for site \(siteID)") - var purchaseOptions: Set = [] - if let appAccountToken = AppAccountToken.tokenWithSiteId(siteID) { - logInfo("Generated appAccountToken \(appAccountToken) for site \(siteID)") - purchaseOptions.insert(.appAccountToken(appAccountToken)) - } - - - logInfo("Purchasing product \(product.id) for site \(siteID) with options \(purchaseOptions)") - logInfo("Pausing transaction listener") - pauseTransactionListener.send(true) - defer { - logInfo("Resuming transaction listener") - pauseTransactionListener.send(false) - } - let purchaseResult = try await product.purchase(options: purchaseOptions) - switch purchaseResult { - case .success(let result): - guard case .verified(let transaction) = result else { - // Ignore unverified transactions. - logError("Transaction unverified: \(result)") - throw Errors.unverifiedTransaction - } - logInfo("Purchased product \(product.id) for site \(siteID): \(transaction)") - - try await submitTransaction(transaction) - await transaction.finish() - completion(.success(purchaseResult)) - case .userCancelled: - logInfo("User cancelled the purchase flow") - completion(.success(purchaseResult)) - case .pending: - logInfo("Purchase returned in a pending state, it might succeed in the future") - pendingTransactionCompletionHandler = completion - @unknown default: - logError("Unknown result for purchase: \(purchaseResult)") - } - } catch { - logError("Error purchasing product \(productID) for site \(siteID): \(error)") - if let purchaseError = error as? StoreKit.Product.PurchaseError { - completion(.failure(Errors.inAppPurchaseProductPurchaseFailed(purchaseError))) - } else if let storeKitError = error as? StoreKitError { - completion(.failure(Errors.inAppPurchaseStoreKitFailed(storeKitError))) - } else { - completion(.failure(error)) - } - } - } - } - - func handleCompletedTransaction(_ result: VerificationResult) async throws { - guard case .verified(let transaction) = result else { - // Ignore unverified transactions. - // TODO: handle errors - logError("Transaction unverified") - return - } - - if let revocationDate = transaction.revocationDate { - // Refunds are handled in the backend - logInfo("Ignoring update about revoked (\(revocationDate)) transaction \(transaction.id)") - } else if let expirationDate = transaction.expirationDate, - expirationDate < Date() { - // Do nothing, this subscription is expired. - logInfo("Ignoring update about expired (\(expirationDate)) transaction \(transaction.id)") - } else if transaction.isUpgraded { - // Do nothing, there is an active transaction - // for a higher level of service. - logInfo("Ignoring update about upgraded transaction \(transaction.id)") - } else { - // Provide access to the product - logInfo("Verified transaction \(transaction.id) (Original ID: \(transaction.originalID)) for product \(transaction.productID)") - try await submitTransaction(transaction) - } - pendingTransactionCompletionHandler?(.success(.success(result))) - pendingTransactionCompletionHandler = nil - logInfo("Marking transaction \(transaction.id) as finished") - await transaction.finish() - } - - func retryWPComSyncForPurchasedProduct(with id: String) async throws { - try await assertInAppPurchasesAreSupported() - - guard let verificationResult = await Transaction.currentEntitlement(for: id) else { - // The user doesn't have a valid entitlement for this product - throw Errors.transactionProductUnknown - } - - guard await Transaction.unfinished.contains(verificationResult) else { - // The transaction is finished. Return successfully - return - } - - try await handleCompletedTransaction(verificationResult) - } - - func assertInAppPurchasesAreSupported() async throws { - guard await inAppPurchasesAreSupported() else { - throw Errors.inAppPurchasesNotSupported - } - } - - func submitTransaction(_ transaction: StoreKit.Transaction) async throws { - guard useBackend else { - return - } - guard let appAccountToken = transaction.appAccountToken else { - throw Errors.transactionMissingAppAccountToken - } - guard let siteID = AppAccountToken.siteIDFromToken(appAccountToken) else { - throw Errors.appAccountTokenMissingSiteIdentifier - } - - let products = try await StoreKit.Product.products(for: [transaction.productID]) - guard let product = products.first else { - throw Errors.transactionProductUnknown - } - let priceInCents = Int(truncating: NSDecimalNumber(decimal: product.price * 100)) - guard let countryCode = await Storefront.current?.countryCode else { - throw Errors.storefrontUnknown - } - - logInfo("Sending transaction to API for site \(siteID)") - do { - let orderID = try await remote.createOrder( - for: siteID, - price: priceInCents, - productIdentifier: product.id, - appStoreCountryCode: countryCode, - originalTransactionId: transaction.originalID, - transactionId: transaction.id, - subscriptionGroupId: transaction.subscriptionGroupID - ) - logInfo("Successfully registered purchase with Order ID \(orderID)") - } catch WordPressApiError.productPurchased { - throw Errors.transactionAlreadyAssociatedWithAnUpgrade - } catch WordPressApiError.transactionReasonInvalid(let reasonMessage) { - /// We ignore transactionReasonInvalid errors, usually these are renewals that - /// MobilePay has already handled via Apple's server to server notifications - /// [See #10075 for details](https://github.com/woocommerce/woocommerce-ios/issues/10075) - logInfo("Unsupported transaction received: \(transaction.id) on site \(siteID), ignoring. \(reasonMessage)") - } catch { - // Rethrow any other error - throw error - } - } - - func userIsEntitledToProduct(with id: String) async throws -> Bool { - guard let verificationResult = await Transaction.currentEntitlement(for: id) else { - // The user hasn't purchased this product. - return false - } - - switch verificationResult { - case .verified: - return true - case .unverified(_, let verificationError): - throw verificationError - } - } - - func getProductIdentifiers() async throws -> [String] { - guard useBackend else { - logInfo("Using hardcoded identifiers") - return Constants.identifiers - } - return try await remote.loadProducts() - } - - func inAppPurchasesAreSupported() async -> Bool { - guard let countryCode = await Storefront.current?.countryCode else { - return false - } - - return supportedCountriesCodes.contains(countryCode) - } - - /// Checks if the Site has current subscriptions via In-App Purchases - /// - func siteHasCurrentInAppPurchases(siteID: Int64) async -> Bool { - for await transaction in Transaction.currentEntitlements { - switch transaction { - case .verified(let transaction): - // If we have current entitlements, we check for its transaction token, and extract the associated siteID. - // If this siteID matches the current siteID, then the site has current In-App Purchases. - guard let token = transaction.appAccountToken, - let transactionSiteID = AppAccountToken.siteIDFromToken(token), - transactionSiteID == siteID else { - continue - } - return true - default: - break - } - } - return false - } - - /// For verified transactions, checks whether a transaction has been handled already on WPCOM end or not - /// we'll mark handled transactions as `finish`. This indicates to the App Store that the app enabled the service to finish the transaction - /// - /// - Parameters: - /// - result: Represents the verification state of an In-App Purchase transaction - /// - transaction: A successful In-App purchase - func handleVerifiedTransactionResult(_ result: VerificationResult, _ transaction: Transaction) async throws { - Task { @MainActor in - // This remote call needs to run in the main thread. Since the request is an AuthenticatedDotcomRequest it requires to instantiate a - // WKWebView and inject a WPCOM token into it as part of the user agent in order to work, however, a WKWebView also requires to be - // ran from the main thread only. This is not assured to happen unless we call the remote through the Action Dispatcher, and - // could cause a runtime crash since there is no compiler-check to stop us from doing so. - // https://github.com/woocommerce/woocommerce-ios/issues/10294 - let wpcomTransactionResponse = try await self.remote.retrieveHandledTransactionResult(for: transaction.id) - if wpcomTransactionResponse.siteID != nil { - await transaction.finish() - self.logInfo("Marking transaction \(transaction.id) as finished") - } else { - try await self.handleCompletedTransaction(result) - self.logInfo("Transaction \(transaction.id) not found in WPCOM") - } - } - } - - func listenForTransactions() { - assert(listenTask == nil, "InAppPurchaseStore.listenForTransactions() called while already listening for transactions") - - listenTask = Task.detached { [weak self] in - guard let self else { - return - } - for await result in Transaction.updates { - switch result { - case .unverified: - // Ignore unverified transactions. - self.logError("Transaction unverified") - break - case .verified(let transaction): - do { - // Wait until the purchase finishes - _ = await self.pauseTransactionListener.values.contains(false) - try await self.handleVerifiedTransactionResult(result, transaction) - } catch { - self.logError("Error handling transaction \(transaction.id) update: \(error)") - } - } - } - } - } - - func logInfo(_ message: String, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line) { - DDLogInfo("[💰IAP Store] \(message)", file: file, function: function, line: line) - } - - func logError(_ message: String, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line) { - DDLogError("[💰IAP Store] \(message)", file: file, function: function, line: line) - } -} - -public extension InAppPurchaseStore { - enum Errors: Error, LocalizedError { - /// The purchase was successful but the transaction was unverified - /// - case unverifiedTransaction - - /// The purchase was successful but it's not associated to an account - /// - case transactionMissingAppAccountToken - - /// The transaction has an associated account but it can't be translated to a site - /// - case appAccountTokenMissingSiteIdentifier - - /// The transaction is associated with an unknown product - /// - case transactionProductUnknown - - /// The storefront for the user is unknown, and so we can't know their country code - /// - case storefrontUnknown - - /// In-app purchases are not supported for this user - /// - case inAppPurchasesNotSupported - - case inAppPurchaseProductPurchaseFailed(StoreKit.Product.PurchaseError) - - case inAppPurchaseStoreKitFailed(StoreKitError) - - case transactionAlreadyAssociatedWithAnUpgrade - - public var errorDescription: String? { - switch self { - case .unverifiedTransaction: - return NSLocalizedString( - "The purchase transaction couldn't be verified", - comment: "Error message used when a purchase was successful but its transaction was unverified") - case .transactionMissingAppAccountToken: - return NSLocalizedString( - "Purchase transaction missing account information", - comment: "Error message used when the purchase transaction doesn't have the right metadata to associate to a specific site") - case .appAccountTokenMissingSiteIdentifier: - return NSLocalizedString( - "Purchase transaction can't be associated to a site", - comment: "Error message used when the purchase transaction doesn't have the right metadata to associate to a specific site") - case .transactionProductUnknown: - return NSLocalizedString( - "Purchase transaction received for an unknown product", - comment: "Error message used when we received a transaction for an unknown product") - case .storefrontUnknown: - return NSLocalizedString( - "Couldn't determine App Store country", - comment: "Error message used when we can't determine the user's App Store country") - case .inAppPurchasesNotSupported: - return NSLocalizedString( - "In-app purchases are not supported for this user yet", - comment: "Error message used when In-app purchases are not supported for this user/site") - case .inAppPurchaseProductPurchaseFailed(let purchaseError): - return NSLocalizedString( - "The In-App Purchase failed, with product purchase error: \(purchaseError)", - comment: "Error message used when a purchase failed") - case .inAppPurchaseStoreKitFailed(let storeKitError): - return NSLocalizedString( - "The In-App Purchase failed, with StoreKit error: \(storeKitError)", - comment: "Error message used when a purchase failed with a store kit error") - case .transactionAlreadyAssociatedWithAnUpgrade: - return NSLocalizedString( - "This In-App purchase was successful, but has already been used to upgrade a site. " + - "Please contact support for more help.", - comment: "Error message shown when the In-App Purchase transaction was already used " + - "for another upgrade – their money was taken, but this site is not upgraded.") - } - } - - public var errorCode: String { - switch self { - case .unverifiedTransaction: - return "iap.T.100" - case .inAppPurchasesNotSupported: - return "iap.T.105" - case .transactionProductUnknown: - return "iap.T.110" - case .inAppPurchaseProductPurchaseFailed(let purchaseError): - switch purchaseError { - case .invalidQuantity: - return "iap.T.115.1" - case .productUnavailable: - return "iap.T.115.2" - case .purchaseNotAllowed: - return "iap.T.115.3" - case .ineligibleForOffer: - return "iap.T.115.4" - case .invalidOfferIdentifier: - return "iap.T.115.5" - case .invalidOfferPrice: - return "iap.T.115.6" - case .invalidOfferSignature: - return "iap.T.115.7" - case .missingOfferParameters: - return "iap.T.115.8" - @unknown default: - return "iap.T.115.0" - } - case .inAppPurchaseStoreKitFailed(let storeKitError): - switch storeKitError { - case .unknown: - return "iap.T.120.1" - case .userCancelled: - return "iap.T.120.2" - case .networkError(let networkError): - return "iap.T.120.3.\(networkError.errorCode)" - case .systemError: - return "iap.T.120.4" - case .notAvailableInStorefront: - return "iap.T.120.5" - case .notEntitled: - return "iap.T.120.6" - @unknown default: - return "iap.T.120.0" - } - case .transactionMissingAppAccountToken: - return "iap.A.100" - case .appAccountTokenMissingSiteIdentifier: - return "iap.A.105" - case .storefrontUnknown: - return "iap.A.110" - case .transactionAlreadyAssociatedWithAnUpgrade: - return "iap.A.115" - } - } - } - - enum Constants { - static let identifiers = [ - "debug.woocommerce.ecommerce.monthly" - ] - } -} diff --git a/Modules/Sources/Yosemite/Tools/InAppPurchases/AppAccountToken.swift b/Modules/Sources/Yosemite/Tools/InAppPurchases/AppAccountToken.swift deleted file mode 100644 index 6898a8a9317..00000000000 --- a/Modules/Sources/Yosemite/Tools/InAppPurchases/AppAccountToken.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -struct AppAccountToken { - static func tokenWithSiteId(_ siteID: Int64) -> UUID? { - let uuidString = String( - format: "%08x-%04x-%04x-%04x-%012x", - // 32 bits for "time_low". - // Backend encodes order_id here, but we don't have an order yet - 0, - // 16 bits for "time_mid" - Int.random(in: 0...Int.max) & 0xfff, - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - Int.random(in: 0...Int.max) & 0x0fff | 0x4000, - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - Int.random(in: 0...Int.max) & 0x3fff | 0x8000, - // 48 bits for "node" - siteID - ) - let uuid = UUID(uuidString: uuidString) - return uuid - } - - static func siteIDFromToken(_ token: UUID) -> Int64? { - let components = token.uuidString.components(separatedBy: "-") - guard components.count == 5, - let siteIdString = components.last else { - return nil - } - let siteId = Int64(siteIdString, radix: 16) - return siteId - } -} diff --git a/Modules/Tests/NetworkingTests/Mapper/InAppPurchaseOrderResultMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/InAppPurchaseOrderResultMapperTests.swift deleted file mode 100644 index a86e8b2958d..00000000000 --- a/Modules/Tests/NetworkingTests/Mapper/InAppPurchaseOrderResultMapperTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import XCTest -@testable import Networking -@testable import NetworkingCore - -final class InAppPurchasesOrderResultMapperTests: XCTestCase { - func test_iap_order_creation_is_decoded_from_json_response() throws { - try XCTSkipIf(true) - - // Given - let jsonData = try XCTUnwrap(Loader.contentsOf("iap-order-create")) - let expectedOrderId = 12345 - - // When - let orderId = try InAppPurchaseOrderResultMapper().map(response: jsonData) - - // Then - assertEqual(expectedOrderId, orderId) - } -} diff --git a/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift deleted file mode 100644 index e73ce5b4263..00000000000 --- a/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -import XCTest -@testable import Networking -@testable import NetworkingCore - -final class InAppPurchasesProductsMapperTests: XCTestCase { - func test_iap_products_list_is_decoded_from_json_response() throws { - try XCTSkipIf(true) - - // Given - let jsonData = try XCTUnwrap(Loader.contentsOf("iap-products")) - let expectedProductIdentifiers = [ - "debug.woocommerce.ecommerce.monthly" - ] - - // When - let products = try InAppPurchasesProductMapper().map(response: jsonData) - - // Then - assertEqual(expectedProductIdentifiers, products) - } -} diff --git a/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesTransactionMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesTransactionMapperTests.swift deleted file mode 100644 index 8e105787a4e..00000000000 --- a/Modules/Tests/NetworkingTests/Mapper/InAppPurchasesTransactionMapperTests.swift +++ /dev/null @@ -1,35 +0,0 @@ -import XCTest -@testable import Networking -@testable import NetworkingCore - -final class InAppPurchasesTransactionMapperTests: XCTestCase { - func test_iap_handled_transaction_is_decoded_from_json_response() throws { - try XCTSkipIf(true) - - // Given - let jsonData = try XCTUnwrap(Loader.contentsOf("iap-transaction-handled")) - let expectedSiteID = Int64(1234) - - // When - let decodedResponse = try InAppPurchasesTransactionMapper().map(response: jsonData) - - // Then - assertEqual(expectedSiteID, decodedResponse.siteID) - } - - func test_iap_unhandled_transaction_is_decoded_from_json_response() throws { - try XCTSkipIf(true) - - // Given - let jsonData = try XCTUnwrap(Loader.contentsOf("iap-transaction-not-handled")) - let expectedErrorMessage = "Transaction not found." - let expectedErrorCode = 404 - - // When - let decodedResponse = try InAppPurchasesTransactionMapper().map(response: jsonData) - - // Then - assertEqual(expectedErrorMessage, decodedResponse.message) - assertEqual(expectedErrorCode, decodedResponse.code) - } -} diff --git a/Modules/Tests/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift b/Modules/Tests/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift deleted file mode 100644 index af358bbc676..00000000000 --- a/Modules/Tests/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift +++ /dev/null @@ -1,154 +0,0 @@ -import XCTest -@testable import Networking -@testable import NetworkingCore - - -/// InAppPurchasesRemote Unit Tests -/// -class InAppPurchasesRemoteTests: XCTestCase { - - /// Dummy Network Wrapper - /// - let network = MockNetwork() - - /// Dummy Site ID - /// - let sampleSiteID: Int64 = 1234 - - /// Dummy Order ID - /// - let sampleOrderId: Int = 12345 - - /// Repeat always! - /// - override func setUp() { - network.removeAllSimulatedResponses() - } - - - /// Verifies that 'moderateComment' as spam properly parses the successful response - /// - func test_load_products_returns_list_of_products() throws { - try XCTSkipIf(true) - - // Given - let remote = InAppPurchasesRemote(network: network) - - network.simulateResponse(requestUrlSuffix: "iap/products", filename: "iap-products") - - // When - var result: Result<[String], Error>? - waitForExpectation { expectation in - remote.loadProducts() { aResult in - result = aResult - expectation.fulfill() - } - } - - // Then - let identifiers = try XCTUnwrap(result?.get()) - XCTAssert(identifiers.count == 1) - } - - func test_purchase_product_returns_created_order() throws { - try XCTSkipIf(true) - - // Given - let remote = InAppPurchasesRemote(network: network) - - network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "iap-order-create") - - // When - var result: Result? - waitForExpectation { expectation in - remote.createOrder( - for: sampleSiteID, - price: 2499, - productIdentifier: "woocommerce_entry_monthly", - appStoreCountryCode: "us", - originalTransactionId: 1234, - transactionId: 12345, - subscriptionGroupId: "21032734") { aResult in - result = aResult - expectation.fulfill() - } - } - - // Then - let orderId = try XCTUnwrap(result?.get()) - XCTAssertEqual(sampleOrderId, orderId) - } - - func test_retrieveHandledTransactionSiteID_when_success_to_retrieve_response_and_transaction_is_handled_then_returns_siteID() throws { - try XCTSkipIf(true) - - // Given - let remote = InAppPurchasesRemote(network: network) - let transactionID: UInt64 = 1234 - - network.simulateResponse(requestUrlSuffix: "iap/transactions/\(transactionID)", filename: "iap-transaction-handled") - - // When - var expectedResult: Result? - waitForExpectation { expectation in - remote.retrieveHandledTransactionResult(for: transactionID) { aResult in - expectedResult = aResult - expectation.fulfill() - } - } - - // Then - let expectedResponse = try XCTUnwrap(expectedResult?.get()) - XCTAssertEqual(expectedResponse.siteID, sampleSiteID) - } - - func test_retrieveHandledTransactionSiteID_when_success_to_retrieve_response_and_transaction_is_not_handled_then_returns_errorResponse() throws { - // Given - let remote = InAppPurchasesRemote(network: network) - let transactionID: UInt64 = 1234 - - network.simulateResponse(requestUrlSuffix: "iap/transactions/\(transactionID)", filename: "iap-transaction-not-handled") - - // When - var expectedResult: Result? - waitForExpectation { expectation in - remote.retrieveHandledTransactionResult(for: transactionID) { aResult in - expectedResult = aResult - expectation.fulfill() - } - } - - // Then - let expectedErrorResponse = try XCTUnwrap(expectedResult?.get()) - XCTAssertEqual(expectedErrorResponse.code, 404) - XCTAssertEqual(expectedErrorResponse.message, "Transaction not found.") - } - - func test_retrieveHandledTransactionSiteID_when_fails_to_retrieve_response_then_returns_network_error() throws { - try XCTSkipIf(true) - - // Given - let remote = InAppPurchasesRemote(network: network) - let transactionID: UInt64 = 1234 - - network.simulateResponse(requestUrlSuffix: "iap/transactions", filename: "") - - // When - var expectedResult: Result? - waitForExpectation { expectation in - remote.retrieveHandledTransactionResult(for: transactionID) { result in - switch result { - case .success(let response): - XCTFail("Expected failure, but found existing handled transaction for associated site ID: \(String(describing: response.siteID))") - case .failure: - expectedResult = result - expectation.fulfill() - } - } - } - - // Then - let expectedError = try XCTUnwrap(expectedResult?.failure) - XCTAssertEqual(expectedError as? NetworkError, Networking.NetworkError.notFound()) - } -} diff --git a/Modules/Tests/YosemiteTests/Stores/FeatureFlagStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/FeatureFlagStoreTests.swift index aea77622264..b2efbb49662 100644 --- a/Modules/Tests/YosemiteTests/Stores/FeatureFlagStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/FeatureFlagStoreTests.swift @@ -67,20 +67,4 @@ final class FeatureFlagStoreTests: XCTestCase { // Then XCTAssertFalse(isEnabled) } - - func test_isRemoteFeatureFlagEnabled_returns_default_value_when_remote_response_does_not_include_input_flag() throws { - // Given - remote.whenLoadingAllFeatureFlags(thenReturn: .success([.hardcodedPlanUpgradeDetailsMilestone1AreAccurate: true])) - - // When - let isEnabled = waitFor { promise in - self.store.onAction(FeatureFlagAction - .isRemoteFeatureFlagEnabled(.storeCreationCompleteNotification, defaultValue: false) { result in - promise(result) - }) - } - - // Then - XCTAssertFalse(isEnabled) - } } diff --git a/Modules/Tests/YosemiteTests/Tools/InAppPurchases/AppAccountTokenTests.swift b/Modules/Tests/YosemiteTests/Tools/InAppPurchases/AppAccountTokenTests.swift deleted file mode 100644 index a12b1bedd44..00000000000 --- a/Modules/Tests/YosemiteTests/Tools/InAppPurchases/AppAccountTokenTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -import XCTest -@testable import Yosemite - -final class AppAccountTokenTests: XCTestCase { - private let sampleSiteId: Int64 = 1234 - - func test_AppAccountToken_encodes_site_id() throws { - let token = AppAccountToken.tokenWithSiteId(sampleSiteId) - let tokenString = try XCTUnwrap(token?.uuidString) - // 0x4D2 is 1234 in hex - XCTAssertTrue(tokenString.hasSuffix("-0000000004D2")) - } - - func test_AppAccountToken_decodes_site_id() throws { - // Random UUID, ensuring last component represents sampleSiteID - // 0x4D2 is 1234 in hex - let token = UUID(uuidString: "2BE1D4A7-0126-438A-A525-0000000004D2")! - let siteID = try XCTUnwrap(AppAccountToken.siteIDFromToken(token)) - XCTAssertEqual(siteID, sampleSiteId) - } - -} diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+WooApp.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+WooApp.swift index 4044cf056d4..6c97edb9099 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent+WooApp.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent+WooApp.swift @@ -2960,57 +2960,6 @@ extension WooAnalyticsEvent { } } -// MARK: - In-App Purchases -extension WooAnalyticsEvent { - enum InAppPurchases { - enum Keys: String { - case productID = "product_id" - case source - case step - case error - } - - enum Source: String { - case banner - } - - enum Step: String { - case planDetails = "plan_details" - case prePurchaseError = "pre_purchase_error" - case purchaseUpgradeError = "purchase_upgrade_error" - case processing - case completed - } - - static func planUpgradePurchaseButtonTapped(_ productID: String) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .planUpgradePurchaseButtonTapped, - properties: [Keys.productID.rawValue: productID]) - } - - static func planUpgradeScreenLoaded(source: Source) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .planUpgradeScreenLoaded, - properties: [Keys.source.rawValue: source.rawValue]) - } - - static func planUpgradeScreenDismissed(step: Step) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .planUpgradeScreenDismissed, - properties: [Keys.step.rawValue: step.rawValue]) - } - - static func planUpgradePrePurchaseFailed(error: Error) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .planUpgradePurchaseFailed, - properties: [Keys.error.rawValue: error.localizedDescription], - error: error) - } - - static func planUpgradePurchaseFailed(error: Error) -> WooAnalyticsEvent { - WooAnalyticsEvent(statName: .planUpgradePurchaseFailed, - properties: [Keys.error.rawValue: error.localizedDescription], - error: error) - } - } -} - // MARK: - EU Shipping Notice Banner // extension WooAnalyticsEvent { diff --git a/WooCommerce/Classes/Extensions/UIImage+Woo.swift b/WooCommerce/Classes/Extensions/UIImage+Woo.swift index 3d5d8cd4388..6185886c4c1 100644 --- a/WooCommerce/Classes/Extensions/UIImage+Woo.swift +++ b/WooCommerce/Classes/Extensions/UIImage+Woo.swift @@ -717,12 +717,6 @@ extension UIImage { .imageFlippedForRightToLeftLayoutDirection() } - /// Shopping cart Purple - /// - static var shoppingCartFilled: UIImage { - return UIImage(named: "icon-shopping-cart-filled")! - } - /// Bordered Custom Amount /// static var borderedCustomAmount: UIImage { diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index 91090093677..006c51d3423 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -280,14 +280,6 @@ extension WooConstants { /// case shippingCustomsInstructionsForEUCountries = "https://www.usps.com/international/new-eu-customs-rules.htm" - /// In-App Purchases subscriptions management URL - /// - case inAppPurchasesAccountSubscriptionsLink = "https://apps.apple.com/account/subscriptions" - - /// URL for Woo Express, which shows plan details. Note that this includes links to start a free trial and pricing for plans, and is only - /// intended for use as a fallback. We should remove this when we fetch plan data from an API. - case fallbackWooExpressHome = "https://woocommerce.com/express" - /// URL for USPS Hazmat instructions detailing to the user the possible categories and why declaring hazmat materials is mandatory /// case uspsInstructions = "https://www.uspsdelivers.com/hazmat-shipping-safety" diff --git a/WooCommerce/Classes/ViewModels/InAppPurchases/InAppPurchasesError.swift b/WooCommerce/Classes/ViewModels/InAppPurchases/InAppPurchasesError.swift deleted file mode 100644 index 78d0a3ac001..00000000000 --- a/WooCommerce/Classes/ViewModels/InAppPurchases/InAppPurchasesError.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Yosemite - -enum PrePurchaseError: Error { - case fetchError - case entitlementsError - case inAppPurchasesNotSupported - case maximumSitesUpgraded - case userNotAllowedToUpgrade -} - -enum PurchaseUpgradeError: Error { - case inAppPurchaseFailed(WooWPComPlan, InAppPurchaseStore.Errors) - case planActivationFailed(InAppPurchaseStore.Errors) - case unknown - - var analyticErrorDetail: Error { - switch self { - case .inAppPurchaseFailed(_, let error): - return error - case .planActivationFailed(let error): - return error - default: - return self - } - } -} diff --git a/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradeViewState.swift b/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradeViewState.swift deleted file mode 100644 index 0d8e7b42813..00000000000 --- a/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradeViewState.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -enum UpgradeViewState: Equatable { - case loading - case loaded([WooWPComPlan]) - case waiting(WooWPComPlan) - case completed(WooWPComPlan) - case prePurchaseError(PrePurchaseError) - case purchaseUpgradeError(PurchaseUpgradeError) - - var analyticsStep: WooAnalyticsEvent.InAppPurchases.Step? { - switch self { - case .loading: - return nil - case .loaded: - return .planDetails - case .waiting: - return .processing - case .completed: - return .completed - case .prePurchaseError: - return .prePurchaseError - case .purchaseUpgradeError: - return .purchaseUpgradeError - } - } -} - -// MARK: - Equatable conformance -extension UpgradeViewState { - static func ==(lhs: UpgradeViewState, rhs: UpgradeViewState) -> Bool { - switch (lhs, rhs) { - case (.loading, .loading): - return true - case (.prePurchaseError(let lhsError), .prePurchaseError(let rhsError)): - return lhsError == rhsError - default: - // TODO: Needs conformance for the rest of cases - return false - } - } -} diff --git a/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradesViewModel.swift b/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradesViewModel.swift deleted file mode 100644 index 6d2f1d5a4ff..00000000000 --- a/WooCommerce/Classes/ViewModels/InAppPurchases/UpgradesViewModel.swift +++ /dev/null @@ -1,304 +0,0 @@ -import Foundation -import SwiftUI -import Yosemite -import Combine -import protocol WooFoundation.Analytics - -/// ViewModel for the Upgrades View -/// Drives the site's available In-App Purchases plan upgrades -/// -final class UpgradesViewModel: ObservableObject { - - @Published var upgradeViewState: UpgradeViewState = .loading - - @Published var isPurchasing: Bool = false - - private let inAppPurchasesPlanManager: InAppPurchasesForWPComPlansProtocol - private let siteID: Int64 - private let storePlanSynchronizer: StorePlanSynchronizing - private let stores: StoresManager - private let localPlans: [WooPlan] = WooPlan.loadM2HardcodedPlans() - private let analytics: Analytics - - private let notificationCenter: NotificationCenter = NotificationCenter.default - private var applicationDidBecomeActiveObservationToken: NSObjectProtocol? - - private var cancellables: Set = [] - - private var plans: [WooWPComPlan] = [] - - init(siteID: Int64, - inAppPurchasesPlanManager: InAppPurchasesForWPComPlansProtocol = InAppPurchasesForWPComPlansManager(), - storePlanSynchronizer: StorePlanSynchronizing = ServiceLocator.storePlanSynchronizer, - stores: StoresManager = ServiceLocator.stores, - analytics: Analytics = ServiceLocator.analytics) { - self.siteID = siteID - self.inAppPurchasesPlanManager = inAppPurchasesPlanManager - self.storePlanSynchronizer = storePlanSynchronizer - self.stores = stores - self.analytics = analytics - - observeViewStateAndTrackAnalytics() - } - - /// Sync wrapper for `fetchViewData`, so can be called directly from where this - /// ViewModel is referenced, outside of the initializer - /// - @MainActor - func retryFetch() { - Task { - await fetchViewData() - } - } - - /// Sets up the view – validates whether they are eligible to upgrade and shows a plan - /// - @MainActor - func prepareViewModel() async { - do { - guard let site = stores.sessionManager.defaultSite else { - throw PrePurchaseError.fetchError - } - - guard site.isSiteOwner else { - throw PrePurchaseError.userNotAllowedToUpgrade - } - - guard await inAppPurchasesPlanManager.inAppPurchasesAreSupported() else { - throw PrePurchaseError.inAppPurchasesNotSupported - } - - async let wpcomPlans = inAppPurchasesPlanManager.fetchPlans() - async let hardcodedPlanDataIsValid = checkHardcodedPlanDataValidity() - - guard try await hasNoActiveInAppPurchases(for: wpcomPlans) else { - throw PrePurchaseError.maximumSitesUpgraded - } - - plans = try await retrievePlanDetailsIfAvailable([.essentialMonthly, .essentialYearly, .performanceMonthly, .performanceYearly], - from: wpcomPlans, - hardcodedPlanDataIsValid: hardcodedPlanDataIsValid) - guard plans.count > 0 else { - throw PrePurchaseError.fetchError - } - upgradeViewState = .loaded(plans) - } catch let prePurchaseError as PrePurchaseError { - DDLogError("prePurchaseError \(prePurchaseError)") - upgradeViewState = .prePurchaseError(prePurchaseError) - } catch { - DDLogError("fetchPlans \(error)") - upgradeViewState = .prePurchaseError(.fetchError) - } - } - - /// Triggers the purchase of the specified In-App Purchases WPCom plans by the passed plan ID - /// linked to the current site ID - /// - @MainActor - func purchasePlan(with planID: String) async { - isPurchasing = true - defer { - isPurchasing = false - } - - analytics.track(event: .InAppPurchases.planUpgradePurchaseButtonTapped(planID)) - guard let wooWPComPlan = planCanBePurchasedFromCurrentState(with: planID) else { - return - } - - observeInAppPurchaseDrawerDismissal { [weak self] in - /// The drawer gets dismissed when the IAP is cancelled too. That gets dealt with in the `do-catch` - /// below, but this is usually received before the `.userCancelled`, so we need to wait a little - /// before we try to advance to the waiting state. - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) { - guard let self else { return } - /// If the user cancelled, we don't advance to waiting, just stay on the plans screen. - /// If the user reached an error state, we will have moved to `.error(_)`, so we should not advance to waiting. - /// In both the above cases, `isPurchasing` will have moved back to false in the defer block - if self.isPurchasing { - self.upgradeViewState = .waiting(wooWPComPlan) - } - } - } - - do { - let result = try await inAppPurchasesPlanManager.purchasePlan(with: planID, - for: siteID) - stopObservingInAppPurchaseDrawerDismissal() - switch result { - case .userCancelled: - /// `no-op` – if the user cancels, we remain in the `loaded` state. - return - case .success(.verified): - // refreshing the synchronizer removes the Upgrade Now banner by the time the flow is closed - storePlanSynchronizer.reloadPlan() - upgradeViewState = .completed(wooWPComPlan) - default: - // TODO: handle `.success(.unverified(_))` here... somehow - return - } - } catch { - DDLogError("purchasePlan \(error)") - stopObservingInAppPurchaseDrawerDismissal() - guard let recognisedError = error as? InAppPurchaseStore.Errors else { - upgradeViewState = .purchaseUpgradeError(.unknown) - return - } - - switch recognisedError { - case .unverifiedTransaction, - .transactionProductUnknown, - .inAppPurchasesNotSupported, - .inAppPurchaseProductPurchaseFailed, - .inAppPurchaseStoreKitFailed: - upgradeViewState = .purchaseUpgradeError(.inAppPurchaseFailed(wooWPComPlan, recognisedError)) - case .transactionMissingAppAccountToken, - .appAccountTokenMissingSiteIdentifier, - .storefrontUnknown, - .transactionAlreadyAssociatedWithAnUpgrade: - upgradeViewState = .purchaseUpgradeError(.planActivationFailed(recognisedError)) - } - } - } -} - -// MARK: - Helpers -// -private extension UpgradesViewModel { - /// Iterates through all available WPCom plans and checks whether the merchant is entitled to purchase them - /// via In-App Purchases - /// - func hasNoActiveInAppPurchases(for plans: [WPComPlanProduct]) async throws -> Bool { - for plan in plans { - do { - if try await inAppPurchasesPlanManager.userIsEntitledToPlan(with: plan.id) { - return false - } - } catch { - DDLogError("There was an error when loading entitlements: \(error)") - throw PrePurchaseError.entitlementsError - } - } - return true - } - - @MainActor - /// Checks whether the current plan details being displayed to merchants are accurate - /// by reaching the remote feature flag - /// - func checkHardcodedPlanDataValidity() async -> Bool { - return await withCheckedContinuation { continuation in - stores.dispatch(FeatureFlagAction.isRemoteFeatureFlagEnabled( - .hardcodedPlanUpgradeDetailsMilestone1AreAccurate, - defaultValue: true) { isEnabled in - continuation.resume(returning: isEnabled) - }) - } - } - - @MainActor - func fetchViewData() async { - upgradeViewState = .loading - await prepareViewModel() - } - - /// Checks whether a plan can be purchased from the current view state, - /// in which case the `WooWPComPlan` object is returned - /// - func planCanBePurchasedFromCurrentState(with planID: String) -> WooWPComPlan? { - switch upgradeViewState { - case .loaded(let plans): - return plans.first(where: { $0.wpComPlan.id == planID }) - case .purchaseUpgradeError(.inAppPurchaseFailed(let plan, _)): - return plan - default: - return nil - } - } -} - -// MARK: - Notification observers -// -private extension UpgradesViewModel { - /// Observes the `didBecomeActiveNotification` for one invocation of the notification. - /// Using this in the scope of `purchasePlan` tells us when Apple's IAP view has completed. - /// - /// However, it can also be triggered by other actions, e.g. a phone call ending. - /// - /// One good example test is to start an IAP, then background the app and foreground it again - /// before the IAP drawer is shown. You'll see that this notification is received, even though the - /// IAP drawer is then shown on top. Dismissing or completing the IAP will not then trigger this - /// notification again. - /// - /// It's not perfect, but it's what we have. - func observeInAppPurchaseDrawerDismissal(whenFired action: @escaping (() -> Void)) { - applicationDidBecomeActiveObservationToken = notificationCenter.addObserver( - forName: UIApplication.didBecomeActiveNotification, - object: nil, - queue: .main) { [weak self] notification in - action() - self?.stopObservingInAppPurchaseDrawerDismissal() - } - } - - func stopObservingInAppPurchaseDrawerDismissal() { - if let token = applicationDidBecomeActiveObservationToken { - notificationCenter.removeObserver(token) - } - } - - /// Retrieves a specific In-App Purchase WPCom plan from the available products - /// - func retrievePlanDetailsIfAvailable(_ types: [AvailableInAppPurchasesWPComPlans], - from wpcomPlans: [WPComPlanProduct], - hardcodedPlanDataIsValid: Bool) -> [WooWPComPlan] { - let plans: [WooWPComPlan] = types.map { type in - guard let wpcomPlanProduct = wpcomPlans.first(where: { $0.id == type.rawValue }), - let wooPlan = localPlans.first(where: { $0.id == wpcomPlanProduct.id }) else { - return nil - } - return WooWPComPlan(wpComPlan: wpcomPlanProduct, - wooPlan: wooPlan, - hardcodedPlanDataIsValid: hardcodedPlanDataIsValid) - }.compactMap { $0 } - - return plans - } -} - -// MARK: - Analytics observers, and track events -// -extension UpgradesViewModel { - /// Observes the view state and tracks events when this changes - /// - private func observeViewStateAndTrackAnalytics() { - $upgradeViewState.sink { [weak self] state in - switch state { - case .waiting: - self?.analytics.track(.planUpgradeProcessingScreenLoaded) - case .loaded: - self?.analytics.track(.planUpgradeScreenLoaded) - case .completed: - self?.analytics.track(.planUpgradeCompletedScreenLoaded) - case .prePurchaseError(let error): - self?.analytics.track(event: .InAppPurchases.planUpgradePrePurchaseFailed(error: error)) - case .purchaseUpgradeError(let error): - self?.analytics.track(event: .InAppPurchases.planUpgradePurchaseFailed(error: error.analyticErrorDetail)) - default: - break - } - } - .store(in: &cancellables) - } - - func track(_ stat: WooAnalyticsStat) { - analytics.track(stat) - } - - func onDisappear() { - guard let stepTracked = upgradeViewState.analyticsStep else { - return - } - analytics.track(event: .InAppPurchases.planUpgradeScreenDismissed(step: stepTracked)) - } -} diff --git a/WooCommerce/Classes/ViewModels/InAppPurchases/WooWPComPlan.swift b/WooCommerce/Classes/ViewModels/InAppPurchases/WooWPComPlan.swift deleted file mode 100644 index c509e434028..00000000000 --- a/WooCommerce/Classes/ViewModels/InAppPurchases/WooWPComPlan.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Yosemite - -public struct WooWPComPlan: Identifiable { - let wpComPlan: WPComPlanProduct - let wooPlan: WooPlan - let hardcodedPlanDataIsValid: Bool - - public var id: String { - return wpComPlan.id - } - - public var shouldDisplayIsPopularBadge: Bool { - let popularPlans = [ - AvailableInAppPurchasesWPComPlans.performanceMonthly.rawValue, - AvailableInAppPurchasesWPComPlans.performanceYearly.rawValue - ] - return popularPlans.contains(where: { $0 == wpComPlan.id }) - } -} diff --git a/WooCommerce/Classes/ViewRelated/AppCoordinator.swift b/WooCommerce/Classes/ViewRelated/AppCoordinator.swift index a9f6ee8cc75..2e134aa128f 100644 --- a/WooCommerce/Classes/ViewRelated/AppCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/AppCoordinator.swift @@ -21,7 +21,6 @@ final class AppCoordinator { private let pushNotesManager: PushNotesManager private let featureFlagService: FeatureFlagService private let switchStoreUseCase: SwitchStoreUseCaseProtocol - private let upgradesViewPresentationCoordinator: UpgradesViewPresentationCoordinator private var storePickerCoordinator: StorePickerCoordinator? private var authStatesSubscription: AnyCancellable? @@ -42,7 +41,6 @@ final class AppCoordinator { loggedOutAppSettings: LoggedOutAppSettingsProtocol = LoggedOutAppSettings(userDefaults: .standard), pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - upgradesViewPresentationCoordinator: UpgradesViewPresentationCoordinator = UpgradesViewPresentationCoordinator(), switchStoreUseCase: SwitchStoreUseCaseProtocol? = nil, themeInstaller: ThemeInstaller = DefaultThemeInstaller()) { self.window = window @@ -62,7 +60,6 @@ final class AppCoordinator { self.pushNotesManager = pushNotesManager self.featureFlagService = featureFlagService self.switchStoreUseCase = switchStoreUseCase ?? SwitchStoreUseCase(stores: stores, storageManager: storageManager) - self.upgradesViewPresentationCoordinator = upgradesViewPresentationCoordinator authenticationManager.setLoggedOutAppSettings(loggedOutAppSettings) self.themeInstaller = themeInstaller diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBanner.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBanner.swift index a98dc0ffb5d..60d1d7e1ef6 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBanner.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBanner.swift @@ -1,21 +1,5 @@ import SwiftUI -/// Hosting controller for `StorePlanBanner`. -/// -final class StorePlanBannerHostingViewController: UIHostingController { - /// Designated initializer. - /// - init(text: String) { - super.init(rootView: StorePlanBanner(text: text)) - } - - /// Needed for protocol conformance. - /// - required dynamic init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - /// Store Plan Banner. To be used inside the Dashboard. /// struct StorePlanBanner: View { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenter.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenter.swift deleted file mode 100644 index 440a0398437..00000000000 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenter.swift +++ /dev/null @@ -1,169 +0,0 @@ -import Foundation -import Yosemite -import Combine -import UIKit -import protocol Experiments.FeatureFlagService -import protocol WooFoundation.ConnectivityObserver - -/// Presents or hides the store plan info banner at the bottom of the screen. -/// Internally uses the `storePlanSynchronizer` to know when to present or hide the banner. -/// -final class StorePlanBannerPresenter { - /// View controller used to present any action needed by the free trial banner. - /// - private weak var viewController: UIViewController? - - /// View that will contain the banner. - /// - private weak var containerView: UIView? - - /// Current site ID. Needed to present the upgrades web view. - private let siteID: Int64 - - /// Closure invoked when the banner is added or removed. - /// - private var onLayoutUpdated: (_ bannerHeight: CGFloat) -> Void - - /// Holds a reference to the Free Trial Banner view, Needed to be able to remove it when required. - /// - private var storePlanBanner: UIView? - - /// Observable subscription store. - /// - private var subscriptions: Set = [] - - private let stores: StoresManager - private let storePlanSynchronizer: StorePlanSynchronizing - private let connectivityObserver: ConnectivityObserver - - private var inAppPurchasesManager: InAppPurchasesForWPComPlansProtocol - - /// - Parameters: - /// - viewController: View controller used to present any action needed by the free trial banner. - /// - containerView: View that will contain the banner. - /// - onLayoutUpdated: Closure invoked when the banner is added or removed. - init(viewController: UIViewController, - containerView: UIView, - siteID: Int64, - onLayoutUpdated: @escaping (CGFloat) -> Void, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - stores: StoresManager = ServiceLocator.stores, - storePlanSynchronizer: StorePlanSynchronizing = ServiceLocator.storePlanSynchronizer, - connectivityObserver: ConnectivityObserver = ServiceLocator.connectivityObserver, - inAppPurchasesManager: InAppPurchasesForWPComPlansProtocol = InAppPurchasesForWPComPlansManager()) { - self.viewController = viewController - self.containerView = containerView - self.siteID = siteID - self.onLayoutUpdated = onLayoutUpdated - self.stores = stores - self.storePlanSynchronizer = storePlanSynchronizer - self.connectivityObserver = connectivityObserver - self.inAppPurchasesManager = inAppPurchasesManager - observeStorePlan() - observeConnectivity() - } - - /// Reloads the site plan and the banner visibility. - /// - func reloadBannerVisibility() { - storePlanSynchronizer.reloadPlan() - } - - /// Bring banner (if visible) to the front. Useful when some content has hidden it. - /// - func bringBannerToFront() { - guard let containerView, let storePlanBanner else { return } - containerView.bringSubviewToFront(storePlanBanner) - } -} - -private extension StorePlanBannerPresenter { - - /// Observe the store plan and add or remove the banner as appropriate - /// - private func observeStorePlan() { - storePlanSynchronizer.planStatePublisher.removeDuplicates() - .combineLatest(stores.site.removeDuplicates()) - .sink { [weak self] planState, site in - guard let self else { return } - switch planState { - case .loaded(let plan) where plan.isFreeTrial: - // Only add the banner for the free trial plan - let bannerViewModel = FreeTrialBannerViewModel(sitePlan: plan) - Task { @MainActor in - await self.addBanner(contentText: bannerViewModel.message) - } - case .loaded(let plan) where plan.isFreePlan && site?.wasEcommerceTrial == true: - // Show plan expired banner for sites with expired WooExpress plans - Task { @MainActor in - await self.addBanner(contentText: Localization.expiredPlan) - } - case .loading, .failed: - break // `.loading` and `.failed` should not change the banner visibility - default: - self.removeBanner() // All other states should remove the banner - } - } - .store(in: &subscriptions) - } - - /// Hide the banner when there is no internet connection. - /// Reload banner visibility when internet is reachable again. - /// - private func observeConnectivity() { - connectivityObserver.statusPublisher.sink { [weak self] status in - switch status { - case .reachable: - self?.reloadBannerVisibility() - case .notReachable: - self?.removeBanner() - case .unknown: - break // No-op - } - } - .store(in: &subscriptions) - } - - /// Adds a Free Trial bar at the bottom of the container view. - /// - @MainActor - private func addBanner(contentText: String) async { - guard let containerView else { return } - - // Remove any previous banner. - storePlanBanner?.removeFromSuperview() - - let storePlanViewController = StorePlanBannerHostingViewController(text: contentText) - storePlanViewController.view.translatesAutoresizingMaskIntoConstraints = false - - containerView.addSubview(storePlanViewController.view) - NSLayoutConstraint.activate([ - storePlanViewController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), - storePlanViewController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), - storePlanViewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) - ]) - - // Let consumers know that the layout has been updated so their content is not hidden by the `freeTrialViewController`. - DispatchQueue.main.async { - self.onLayoutUpdated(storePlanViewController.view.frame.size.height) - } - - // Store a reference to it to manipulate it later in `removeBanner`. - storePlanBanner = storePlanViewController.view - } - - /// Removes the Free Trial Banner from the container view.. - /// - func removeBanner() { - guard let storePlanBanner else { return } - storePlanBanner.removeFromSuperview() - onLayoutUpdated(.zero) - self.storePlanBanner = nil - } -} - -private extension StorePlanBannerPresenter { - enum Localization { - static let expiredPlan = NSLocalizedString("Your site plan has ended.", comment: "Title on the banner when the site's WooExpress plan has expired") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Onboarding/StoreOnboardingCoordinator.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Onboarding/StoreOnboardingCoordinator.swift index 3dcc5268355..5cb8e3abc0e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Onboarding/StoreOnboardingCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Onboarding/StoreOnboardingCoordinator.swift @@ -159,8 +159,8 @@ private extension StoreOnboardingCoordinator { /// Navigates the user to the plan subscription details view. /// func showPlanView() { - let subscriptionController = SubscriptionsHostingController(siteID: site.siteID) - navigationController.show(subscriptionController, sender: self) + // No longer needed. + // To be removed with continuation of 12401-gh } } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index ef436d41012..58352cfa501 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -141,8 +141,6 @@ private extension HubMenu { .navigationTitle(Localization.reviews) case .coupons: couponListView - case .subscriptions: - SubscriptionsView(viewModel: .init()) case .customers: CustomersListView(viewModel: .init(siteID: viewModel.siteID)) case .reviewDetails(let parcel): diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index fc0310b19d0..f0aba15a7ae 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -26,7 +26,6 @@ enum HubMenuNavigationDestination: Hashable { case inbox case reviews case coupons - case subscriptions case customers case reviewDetails(parcel: ProductReviewFromNoteParcel) } @@ -243,15 +242,6 @@ final class HubMenuViewModel: ObservableObject { private extension HubMenuViewModel { func setupSettingsElements() { settingsElements = [Settings()] - - guard let site = stores.sessionManager.defaultSite, - // Only show the upgrades menu on WPCom sites and non free trial sites - site.isWordPressComStore, - !site.isFreeTrialSite else { - return - } - - settingsElements.append(Subscriptions()) } func setupGeneralElements() { @@ -632,19 +622,6 @@ extension HubMenuViewModel { let navigationDestination: HubMenuNavigationDestination? = .reviews } - struct Subscriptions: HubMenuItem { - static var id = "subscriptions" - - let title: String = Localization.subscriptions - let description: String = Localization.subscriptionsDescription - let icon: UIImage = .shoppingCartFilled - let iconColor: UIColor = .primary - let accessibilityIdentifier: String = "menu-subscriptions" - let trackingOption: String = "upgrades" - let iconBadge: HubMenuBadgeType? = nil - let navigationDestination: HubMenuNavigationDestination? = .subscriptions - } - struct Customers: HubMenuItem { static var id = "customers" @@ -749,14 +726,6 @@ extension HubMenuViewModel { "Capture reviews for your store", comment: "Description of one of the hub menu options") - static let subscriptions = NSLocalizedString( - "Subscriptions", - comment: "Title of one of the hub menu options") - - static let subscriptionsDescription = NSLocalizedString( - "Manage your subscription", - comment: "Description of one of the hub menu options") - static let customers = NSLocalizedString( "hubMenu.customers", value: "Customers", diff --git a/WooCommerce/Classes/ViewRelated/InAppPurchases/InAppPurchasesForWPComPlansManager.swift b/WooCommerce/Classes/ViewRelated/InAppPurchases/InAppPurchasesForWPComPlansManager.swift deleted file mode 100644 index dee2f3dea64..00000000000 --- a/WooCommerce/Classes/ViewRelated/InAppPurchases/InAppPurchasesForWPComPlansManager.swift +++ /dev/null @@ -1,118 +0,0 @@ -import Foundation -import StoreKit -import Yosemite - -protocol WPComPlanProduct { - // The localized product name, to be used as title in UI - var displayName: String { get } - // The localized product description - var description: String { get } - // The unique product identifier. To be used in further actions e.g purchasing a product - var id: String { get } - // The localized price, including currency - var displayPrice: String { get } -} - -extension StoreKit.Product: WPComPlanProduct {} - -typealias InAppPurchaseResult = StoreKit.Product.PurchaseResult - -protocol InAppPurchasesForWPComPlansProtocol { - /// Retrieves asynchronously all WPCom plans In-App Purchases products. - /// - func fetchPlans() async throws -> [WPComPlanProduct] - - /// Returns whether the user is entitled the WPCom plan identified with the passed id. - /// - /// - Parameters: - /// - id: the id of the WPCom plan whose entitlement is to be verified - /// - func userIsEntitledToPlan(with id: String) async throws -> Bool - - /// Triggers the purchase of WPCom plan specified by the passed product id, linked to the passed site Id. - /// - /// - Parameters: - /// id: the id of the WPCom plan to be purchased - /// remoteSiteId: the id of the site linked to the purchasing plan - /// - func purchasePlan(with id: String, for remoteSiteId: Int64) async throws -> InAppPurchaseResult - - /// Retries forwarding the WPCom plan purchase to our backend, so the plan can be unlocked. - /// This can happen when the purchase was previously successful but unlocking the WPCom plan request - /// failed. - /// - /// - Parameters: - /// id: the id of the purchased WPCom plan whose unlock failed - /// - func retryWPComSyncForPurchasedPlan(with id: String) async throws - - /// Returns whether In-App Purchases are supported for the current user configuration - /// - func inAppPurchasesAreSupported() async -> Bool - - /// Returns whether the site has any current In-App Purchases product - /// - func siteHasCurrentInAppPurchases(siteID: Int64) async -> Bool -} - -final class InAppPurchasesForWPComPlansManager: InAppPurchasesForWPComPlansProtocol { - private let stores: StoresManager - - init(stores: StoresManager = ServiceLocator.stores) { - self.stores = stores - } - - @MainActor - func fetchPlans() async throws -> [WPComPlanProduct] { - try await withCheckedThrowingContinuation { continuation in - stores.dispatch(InAppPurchaseAction.loadProducts(completion: { result in - continuation.resume(with: result) - })) - } - } - - @MainActor - func userIsEntitledToPlan(with id: String) async throws -> Bool { - try await withCheckedThrowingContinuation { continuation in - stores.dispatch(InAppPurchaseAction.userIsEntitledToProduct(productID: id, completion: { result in - continuation.resume(with: result) - })) - } - } - - @MainActor - func purchasePlan(with id: String, for remoteSiteId: Int64) async throws -> InAppPurchaseResult { - try await withCheckedThrowingContinuation { continuation in - stores.dispatch(InAppPurchaseAction.purchaseProduct(siteID: remoteSiteId, productID: id, completion: { result in - continuation.resume(with: result) - })) - } - } - - @MainActor - func retryWPComSyncForPurchasedPlan(with id: String) async throws { - try await withCheckedThrowingContinuation { continuation in - stores.dispatch(InAppPurchaseAction.retryWPComSyncForPurchasedProduct(productID: id, completion: { result in - continuation.resume(with: result) - })) - } - } - - @MainActor - func inAppPurchasesAreSupported() async -> Bool { - await withCheckedContinuation { continuation in - stores.dispatch(InAppPurchaseAction.inAppPurchasesAreSupported(completion: { result in - continuation.resume(returning: result) - })) - } - } - - @MainActor - func siteHasCurrentInAppPurchases(siteID: Int64) async -> Bool { - await withCheckedContinuation { continuation in - stores.dispatch(InAppPurchaseAction.siteHasCurrentInAppPurchases(siteID: siteID) { result in - continuation.resume(returning: result) - }) - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift index 875cf770d8e..474b4e80646 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift @@ -144,10 +144,6 @@ final class OrderListViewController: UIViewController, GhostableViewController { /// private var inPersonPaymentsSurveyVariation: SurveyViewController.Source? - /// Store plan banner presentation handler. - /// - private var storePlanBannerPresenter: StorePlanBannerPresenter? - /// Notice presentation handler /// private var noticePresenter: NoticePresenter = DefaultNoticePresenter() @@ -190,8 +186,6 @@ final class OrderListViewController: UIViewController, GhostableViewController { configureViewModel() configureSyncingCoordinator() - - configureStorePlanBannerPresenter() } private func createDataSource() { @@ -367,14 +361,6 @@ private extension OrderListViewController { let headerType = TwoColumnSectionHeaderView.self tableView.register(headerType.loadNib(), forHeaderFooterViewReuseIdentifier: headerType.reuseIdentifier) } - - func configureStorePlanBannerPresenter() { - self.storePlanBannerPresenter = StorePlanBannerPresenter(viewController: self, - containerView: view, - siteID: siteID) { [weak self] bannerHeight in - self?.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bannerHeight, right: 0) - } - } } // MARK: - Actions @@ -729,9 +715,6 @@ private extension OrderListViewController { childView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor) ]) childController.didMove(toParent: self) - - // Make sure the banner is on top of the empty state view - storePlanBannerPresenter?.bringBannerToFront() } func removeEmptyViewController() { diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 29a81edcc17..d27b02315bb 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -195,10 +195,6 @@ final class ProductsViewController: UIViewController, GhostableViewController { /// @Published private var dataLoadingError: Error? - /// Store plan banner presentation handler. - /// - private var storePlanBannerPresenter: StorePlanBannerPresenter? - private var subscriptions: Set = [] private var addProductCoordinator: AddProductCoordinator? @@ -250,7 +246,6 @@ final class ProductsViewController: UIViewController, GhostableViewController { configureToolbar() configureScrollWatcher() configurePaginationTracker() - configureStorePlanBannerPresenter() registerTableViewCells() showTopBannerViewIfNeeded() @@ -802,14 +797,6 @@ private extension ProductsViewController { toolbar.isHidden = filters.numberOfActiveFilters == 0 ? isEmpty : false } - - func configureStorePlanBannerPresenter() { - self.storePlanBannerPresenter = StorePlanBannerPresenter(viewController: self, - containerView: view, - siteID: siteID) { [weak self] bannerHeight in - self?.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bannerHeight, right: 0) - } - } } // MARK: - Updates @@ -1314,9 +1301,6 @@ private extension ProductsViewController { let config = createFilterConfig() displayEmptyStateViewController(emptyStateViewController) emptyStateViewController.configure(config) - - // Make sure the banner is on top of the empty state view - storePlanBannerPresenter?.bringBannerToFront() } func createFilterConfig() -> EmptyStateViewController.Config { diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/CompletedUpgradeView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/CompletedUpgradeView.swift deleted file mode 100644 index d443ea7a9bb..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/CompletedUpgradeView.swift +++ /dev/null @@ -1,94 +0,0 @@ -import SwiftUI - -struct CompletedUpgradeView: View { - // Confetti animation runs on any change of this variable - @State private var confettiTrigger: Int = 0 - - let planName: String - - let doneAction: (() -> Void) - - var body: some View { - VStack { - ScrollView(.vertical) { - VStack(spacing: Layout.groupSpacing) { - Image("plan-upgrade-success-celebration") - .frame(maxWidth: .infinity, alignment: .center) - .accessibilityHidden(true) - - VStack(spacing: Layout.textSpacing) { - Text(Localization.title) - .font(.largeTitle) - .fontWeight(.bold) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - Text(LocalizedString(format: Localization.subtitle, planName)) - .font(.title3) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - - Text(Localization.hint) - .font(.footnote) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - .padding(.top, Layout.completedUpgradeViewTopPadding) - .padding(.horizontal, Layout.padding) - } - - Spacer() - - Button(Localization.doneButtonText) { - doneAction() - } - .buttonStyle(PrimaryButtonStyle()) - .padding(.horizontal, Layout.padding) - } - .confettiCannon(counter: $confettiTrigger, - num: Constants.numberOfConfettiElements, - colors: [.withColorStudio(name: .wooCommercePurple, shade: .shade10), - .withColorStudio(name: .wooCommercePurple, shade: .shade30), - .withColorStudio(name: .wooCommercePurple, shade: .shade70), - .withColorStudio(name: .wooCommercePurple, shade: .shade80)], - radius: Constants.confettiRadius) - .onAppear { - confettiTrigger += 1 - } - .padding(.bottom, Layout.padding) - } -} - -private extension CompletedUpgradeView { - struct Layout { - static let completedUpgradeViewTopPadding: CGFloat = 70 - static let padding: CGFloat = 16 - static let groupSpacing: CGFloat = 32 - static let textSpacing: CGFloat = 16 - } - - struct Constants { - static let numberOfConfettiElements: Int = 100 - static let confettiRadius: CGFloat = 500 - } - - enum Localization { - static let title = NSLocalizedString( - "Woo! You’re off to a great start!", - comment: "Text shown when a plan upgrade has been successfully purchased.") - - static let subtitle = NSLocalizedString( - "Your purchase is complete and you're on the %1$@ plan.", - comment: "Additional text shown when a plan upgrade has been successfully purchased. %1$@ is replaced by " + - "the plan name, and should be included in the translated string.") - - static let hint = NSLocalizedString( - "You can manage your subscription in your iPhone Settings → Your Name → Subscriptions", - comment: "Instructions guiding the merchant to manage a site's plan upgrade.") - - static let doneButtonText = NSLocalizedString( - "Done", - comment: "Done button on the screen that is shown after a successful plan upgrade.") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/CurrentPlanDetailsView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/CurrentPlanDetailsView.swift deleted file mode 100644 index 83c27914d70..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/CurrentPlanDetailsView.swift +++ /dev/null @@ -1,71 +0,0 @@ -import SwiftUI -import WooFoundation - -struct CurrentPlanDetailsView: View { - @State var expirationDate: String? - @State var daysLeft: Int? - - private var daysLeftText: String { - guard let daysLeft else { - return "" - } - return String.pluralize(daysLeft, - singular: Localization.daysLeftValueSingular, - plural: Localization.daysLeftValuePlural) - } - - var body: some View { - VStack(alignment: .leading, spacing: Layout.contentSpacing) { - if let expirationDate = expirationDate { - Text(Localization.freeTrialTitle) - .font(.title2.bold()) - .accessibilityAddTraits(.isHeader) - Text(String.localizedStringWithFormat(Localization.freeTrialText, daysLeftText, expirationDate)) - .font(.footnote) - } else { - Text(Localization.freeTrialHasEndedTitle) - .font(.title2.bold()) - .accessibilityAddTraits(.isHeader) - Text(Localization.freeTrialExpiredText) - .font(.footnote) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding([.leading, .trailing]) - .padding(.vertical, Layout.smallPadding) - } -} - -private extension CurrentPlanDetailsView { - struct Layout { - static let contentSpacing: CGFloat = 8 - static let smallPadding: CGFloat = 8 - } - - enum Localization { - static let freeTrialTitle = NSLocalizedString( - "You're in a free trial", - comment: "Title for the Upgrades summary card, informing the merchant they're on a Free Trial site.") - - static let freeTrialHasEndedTitle = NSLocalizedString( - "Your free trial has ended", - comment: "Title for the Upgrades summary card, informing the merchant their Free Trial has ended.") - - static let freeTrialText = NSLocalizedString( - "Your free trial will end in %@. Upgrade to a plan by %@ to unlock new features and start selling.", - comment: "Text within the Upgrades summary card, informing the merchant of how much time they have to upgrade.") - - static let freeTrialExpiredText = NSLocalizedString( - "Don't lose all that hard work! Upgrade to a paid plan to continue working on your store. " + - "Unlock more features, launch and start selling, and make your ecommerce business a reality.", - comment: "Text within the Upgrades summary card, informing the merchant their Free Trial has expired.") - - static let daysLeftValuePlural = NSLocalizedString( - "%1ld days", comment: "Value describing the days left on a plan before expiry (plural). " + - "%1ld must be included in the translation, and will be replaced with the count. Reads as '15 days'") - - static let daysLeftValueSingular = NSLocalizedString( - "%1$ld day", comment: "Value describing the days left on a plan before expiry (singular). " + - "%1ld must be included in the translation, and will be replaced with the count. Reads as '1 day'") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift deleted file mode 100644 index 7a10278e72c..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import Foundation -import SwiftUI - -struct FullFeatureListView: View { - @Environment(\.presentationMode) var presentationMode - - var featureListGroups = FullFeatureListViewModel.hardcodedFullFeatureList() - - var body: some View { - ScrollView() { - VStack(alignment: .leading, spacing: FullFeatureListView.Layout.featureListSpacing) { - ForEach(featureListGroups, id: \.title) { featureList in - Text(featureList.title) - .font(.title) - .bold() - .padding(.top) - .padding(.bottom) - .accessibilityAddTraits(.isHeader) - 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) - .accessibilityRemoveTraits([.isImage]) - .accessibilityLabel(Localization.performanceOnlyText) - } - } - Divider() - .padding(.top) - .padding(.bottom) - HStack { - Image(systemName: "star.fill") - Text(Localization.performanceOnlyText) - } - .font(.footnote) - .foregroundColor(.withColorStudio(name: .wooCommercePurple, shade: .shade50)) - .padding(.bottom) - .renderedIf(featureList.performanceFeatures.isNotEmpty) - .accessibilityHidden(true) - } - } - .padding(.horizontal) - .background(Color(.systemGroupedBackground)) - .cornerRadius(Layout.featureListCornerRadius) - VStack(alignment: .leading, spacing: Layout.featureListSpacing) { - Text(Localization.paymentsDisclaimerText) - .font(.caption) - Text(Localization.pluginsDisclaimerText) - .font(.caption) - } - .background(Color(.secondarySystemBackground)) - .padding(.top) - } - .padding() - .navigationTitle(Localization.featureListTitleText) - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(leading: Button(action: { - presentationMode.wrappedValue.dismiss() - }) { - Image(systemName: "chevron.backward") - }) - .background(Color(.secondarySystemBackground)) - } -} - -private extension FullFeatureListView { - struct Localization { - static let featureListTitleText = NSLocalizedString( - "Full Feature List", - comment: "Title of the view which shows the full feature list for paid plans.") - - static let performanceOnlyText = NSLocalizedString( - "Performance plan only", - comment: "Indicates a feature is part of the performance plan.") - - static let paymentsDisclaimerText = NSLocalizedString( - "1. Available as standard in WooCommerce Payments (restrictions apply)." + - "Additional extensions may be required for other payment providers.", - 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: "Disclaimer regarding some of the features related to shipping.") - } - - struct Layout { - 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 deleted file mode 100644 index 204044fc62f..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/FullFeatureListViewModel.swift +++ /dev/null @@ -1,279 +0,0 @@ -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 { - 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: featureGroupTitleComment) - - static let productsFeatureTitle = NSLocalizedString( - "Products", - comment: featureGroupTitleComment) - - static let paymentsFeatureTitle = NSLocalizedString( - "Payments", - comment: featureGroupTitleComment) - - static let marketingAndEmailFeatureTitle = NSLocalizedString( - "Marketing & Email", - comment: featureGroupTitleComment) - - static let shippingFeatureTitle = NSLocalizedString( - "Shipping", - comment: featureGroupTitleComment) - - static let wooCommerceStoreText = NSLocalizedString( - "WooCommerce store", - comment: essentialFeatureTitleComment) - - static let wooCommerceMobileAppText = NSLocalizedString( - "WooCommerce mobile app", - comment: essentialFeatureTitleComment) - - static let wordPressCMSText = NSLocalizedString( - "WordPress CMS", - comment: essentialFeatureTitleComment) - - static let wordPressMobileAppText = NSLocalizedString( - "WordPress mobile app", - comment: essentialFeatureTitleComment) - - static let freeSSLCertificateText = NSLocalizedString( - "Free SSL certificate", - comment: essentialFeatureTitleComment) - - static let generousStorageText = NSLocalizedString( - "Generous storage", - comment: essentialFeatureTitleComment) - - static let automatedBackupQuickRestoreText = NSLocalizedString( - "Automated backup + quick restore", - comment: essentialFeatureTitleComment) - - static let adFreeExperienceText = NSLocalizedString( - "Ad-free experience", - comment: essentialFeatureTitleComment) - - static let unlimitedAdminAccountsText = NSLocalizedString( - "Unlimited admin accounts", - comment: essentialFeatureTitleComment) - - static let liveChatSupportText = NSLocalizedString( - "Live chat support", - comment: essentialFeatureTitleComment) - - static let emailSupportText = NSLocalizedString( - "Email support", - comment: essentialFeatureTitleComment) - - static let premiumThemesIncludedText = NSLocalizedString( - "Premium themes included", - comment: essentialFeatureTitleComment) - - static let salesReportsText = NSLocalizedString( - "Sales reports", - comment: essentialFeatureTitleComment) - - static let googleAnalyticsText = NSLocalizedString( - "Google Analytics", - comment: essentialFeatureTitleComment) - - static let listUnlimitedProducts = NSLocalizedString( - "List unlimited products", - comment: essentialFeatureTitleComment) - - static let giftCards = NSLocalizedString( - "Gift cards", - comment: essentialFeatureTitleComment) - - static let listProductsByBrand = NSLocalizedString( - "List products by brand", - comment: essentialFeatureTitleComment) - - static let integratedPayments = NSLocalizedString( - "Integrated payments", - comment: essentialFeatureTitleComment) - - static let internationalPayments = NSLocalizedString( - "International payments'", - comment: essentialFeatureTitleComment) - - static let automatedSalesTaxes = NSLocalizedString( - "Automated sales taxes", - comment: essentialFeatureTitleComment) - - static let acceptLocalPayments = NSLocalizedString( - "Accept local payments'", - comment: essentialFeatureTitleComment) - - static let recurringPayments = NSLocalizedString( - "Recurring payments'", - comment: essentialFeatureTitleComment) - - static let promoteOnTikTok = NSLocalizedString( - "Promote on TikTok", - comment: essentialFeatureTitleComment) - - static let syncWithPinterest = NSLocalizedString( - "Sync with Pinterest", - comment: essentialFeatureTitleComment) - - static let connectWithFacebook = NSLocalizedString( - "Connect with Facebook", - comment: essentialFeatureTitleComment) - - static let advancedSeoTools = NSLocalizedString( - "Advanced SEO tools", - comment: essentialFeatureTitleComment) - - static let advertiseOnGoogle = NSLocalizedString( - "Advertise on Google", - comment: essentialFeatureTitleComment) - - static let customOrderEmails = NSLocalizedString( - "Custom order emails", - comment: essentialFeatureTitleComment) - - static let shipmentTracking = NSLocalizedString( - "Shipment tracking", - comment: essentialFeatureTitleComment) - - static let liveShippingRates = NSLocalizedString( - "Live shipping rates", - comment: essentialFeatureTitleComment) - - static let printShippingLabels = NSLocalizedString( - "Print shipping labels²", - comment: essentialFeatureTitleComment) - - static let minMaxOrderQuantityText = NSLocalizedString( - "Min/Max order quantity", - comment: performanceFeatureTitleComment) - - static let productBundlesText = NSLocalizedString( - "Product Bundles", - comment: performanceFeatureTitleComment) - - static let customProductKitsText = NSLocalizedString( - "Custom product kits", - comment: performanceFeatureTitleComment) - - static let productRecommendationsText = NSLocalizedString( - "Product recommendations", - comment: performanceFeatureTitleComment) - - static let backInStockEmailsText = NSLocalizedString( - "Back in stock emails", - comment: performanceFeatureTitleComment) - - static let marketingAutomationText = NSLocalizedString( - "Marketing automation", - comment: performanceFeatureTitleComment) - - static let abandonedCartRecoveryText = NSLocalizedString( - "Abandoned cart recovery", - comment: performanceFeatureTitleComment) - - static let referralProgramsText = NSLocalizedString( - "Referral programs", - comment: performanceFeatureTitleComment) - - static let customerBirthdayEmailsText = NSLocalizedString( - "Customer birthday emails", - comment: performanceFeatureTitleComment) - - static let loyaltyPointsProgramsText = NSLocalizedString( - "Loyalty points programs", - comment: performanceFeatureTitleComment) - - static let discountedShippingText = NSLocalizedString( - "Discounted shipping²", - comment: performanceFeatureTitleComment) - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift deleted file mode 100644 index 669dd157abb..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/OwnerUpgradesView.swift +++ /dev/null @@ -1,156 +0,0 @@ -import SwiftUI -import Yosemite -import WooFoundation - -struct OwnerUpgradesView: View { - - @State var upgradePlans: [WooWPComPlan] - @Binding var isPurchasing: Bool - @Binding var expirationDate: String? - @Binding var planDaysLeft: Int? - let purchasePlanAction: (WooWPComPlan) -> Void - @State var isLoading: Bool - - init(upgradePlans: [WooWPComPlan], - isPurchasing: Binding, - expirationDate: Binding, - planDaysLeft: Binding, - purchasePlanAction: @escaping ((WooWPComPlan) -> Void), - isLoading: Bool = false) { - _upgradePlans = .init(initialValue: upgradePlans) - _isPurchasing = isPurchasing - _expirationDate = expirationDate - _planDaysLeft = planDaysLeft - self.purchasePlanAction = purchasePlanAction - _isLoading = .init(initialValue: isLoading) - } - - @State private var paymentFrequency: WooPlan.PlanFrequency = .month - private var paymentFrequencies: [WooPlan.PlanFrequency] = [.month, .year] - - @State var selectedPlan: WooWPComPlan? = nil - @State private var showingFullFeatureList = false - - var body: some View { - VStack(spacing: 0) { - VStack { - CurrentPlanDetailsView(expirationDate: expirationDate, - daysLeft: planDaysLeft) - .background(Color(.secondarySystemGroupedBackground)) - } - .padding(.horizontal) - .cornerRadius(Layout.cornerRadius) - .background(Color(.systemGroupedBackground)) - .redacted(reason: isLoading ? .placeholder : []) - - Picker(selection: $paymentFrequency, label: EmptyView()) { - ForEach(paymentFrequencies) { - Text($0.paymentFrequencyLocalizedString) - } - } - .pickerStyle(.segmented) - .disabled(isLoading) - .padding() - .background(Color(.systemGroupedBackground)) - .redacted(reason: isLoading ? .placeholder : []) - - ScrollView { - VStack { - ForEach(upgradePlans.filter { $0.wooPlan.planFrequency == paymentFrequency }) { upgradePlan in - WooPlanCardView(upgradePlan: upgradePlan, selectedPlan: $selectedPlan) - .disabled(isPurchasing) - .listRowSeparator(.hidden) - .redacted(reason: isLoading ? .placeholder : []) - .padding(.bottom, 8) - } - Button(Localization.allFeaturesListText) { - showingFullFeatureList.toggle() - } - .buttonStyle(SecondaryButtonStyle()) - .disabled(isPurchasing || isLoading) - .redacted(reason: isLoading ? .placeholder : []) - .sheet(isPresented: $showingFullFeatureList) { - NavigationView { - FullFeatureListView() - } - } - } - .padding() - } - .background(Color(.systemGroupedBackground)) - - VStack { - if let selectedPlan { - let buttonText = String.localizedStringWithFormat(Localization.purchaseCTAButtonText, selectedPlan.wpComPlan.displayName) - Button(buttonText) { - purchasePlanAction(selectedPlan) - } - .buttonStyle(PrimaryLoadingButtonStyle(isLoading: isPurchasing)) - .disabled(isLoading) - .redacted(reason: isLoading ? .placeholder : []) - } else { - Button(Localization.selectPlanButtonText) { - // no-op - } - .buttonStyle(PrimaryLoadingButtonStyle(isLoading: isPurchasing)) - .disabled(true) - .redacted(reason: isLoading ? .placeholder : []) - } - } - .padding() - } - .background(Color(.systemGroupedBackground)) - } -} - -private extension OwnerUpgradesView { - enum Layout { - static let cornerRadius: CGFloat = 8.0 - } -} - -private extension OwnerUpgradesView { - struct Localization { - static let purchaseCTAButtonText = NSLocalizedString( - "Purchase %1$@", - comment: "The title of the button to purchase a Plan." + - "Reads as 'Purchase Essential Monthly'") - - static let featuresHeaderTextFormat = NSLocalizedString( - "Get the most out of %1$@", - comment: "Title for the section header for the list of feature categories on the Upgrade plan screen. " + - "Reads as 'Get the most out of Essential'. %1$@ must be included in the string and will be replaced with " + - "the plan name.") - - static let featureDetailsUnavailableText = NSLocalizedString( - "See plan details", comment: "Title for a link to view Woo Express plan details on the web, as a fallback.") - - 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.") - } -} - -private extension WooPlan.PlanFrequency { - var paymentFrequencyLocalizedString: String { - switch self { - case .month: - return Localization.payMonthly - case .year: - return Localization.payAnnually - } - } - - enum Localization { - static let payMonthly = NSLocalizedString( - "Monthly", - comment: "Title of the selector option for paying monthly on the Upgrade view, when choosing a plan") - - static let payAnnually = NSLocalizedString( - "Annually (Save 35%)", - comment: "Title of the selector option for paying annually on the Upgrade view, when choosing a plan") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/PrePurchaseUpgradesErrorView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/PrePurchaseUpgradesErrorView.swift deleted file mode 100644 index ef127c0f204..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/PrePurchaseUpgradesErrorView.swift +++ /dev/null @@ -1,121 +0,0 @@ -import SwiftUI - -struct PrePurchaseUpgradesErrorView: View { - - private let error: PrePurchaseError - - /// Closure invoked when the "Retry" button is tapped - /// - var onRetryButtonTapped: (() -> Void) - - init(_ error: PrePurchaseError, - onRetryButtonTapped: @escaping (() -> Void)) { - self.error = error - self.onRetryButtonTapped = onRetryButtonTapped - } - - var body: some View { - VStack(alignment: .center, spacing: Layout.spacingBetweenImageAndText) { - Image("plan-upgrade-error") - .frame(maxWidth: .infinity, alignment: .center) - .accessibilityHidden(true) - - VStack(alignment: .center, spacing: Layout.textSpacing) { - switch error { - case .fetchError, .entitlementsError: - VStack(alignment: .center) { - Text(Localization.fetchErrorMessage) - .bold() - .headlineStyle() - .multilineTextAlignment(.center) - Button(Localization.retry) { - onRetryButtonTapped() - } - .buttonStyle(PrimaryButtonStyle()) - .fixedSize(horizontal: true, vertical: true) - } - case .maximumSitesUpgraded: - Text(Localization.maximumSitesUpgradedErrorMessage) - .bold() - .headlineStyle() - .multilineTextAlignment(.center) - Text(Localization.maximumSitesUpgradedErrorSubtitle) - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - case .inAppPurchasesNotSupported: - Text(Localization.inAppPurchasesNotSupportedErrorMessage) - .bold() - .headlineStyle() - .multilineTextAlignment(.center) - Text(Localization.inAppPurchasesNotSupportedErrorSubtitle) - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - case .userNotAllowedToUpgrade: - Text(Localization.unableToUpgradeText) - .bold() - .headlineStyle() - .multilineTextAlignment(.center) - Text(Localization.unableToUpgradeInstructions) - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - } - } - .padding(.horizontal, Layout.horizontalEdgesPadding) - .padding(.vertical, Layout.verticalEdgesPadding) - .background { - RoundedRectangle(cornerSize: .init(width: Layout.cornerRadius, height: Layout.cornerRadius)) - .fill(Color(.secondarySystemGroupedBackground)) - } - } -} - -private extension PrePurchaseUpgradesErrorView { - enum Layout { - static let horizontalEdgesPadding: CGFloat = 16 - static let verticalEdgesPadding: CGFloat = 40 - static let cornerRadius: CGFloat = 12 - static let spacingBetweenImageAndText: CGFloat = 32 - static let textSpacing: CGFloat = 16 - } - - enum Localization { - static let retry = NSLocalizedString( - "Retry", comment: "Title of the button to attempt a retry when fetching or purchasing plans fails.") - - static let fetchErrorMessage = NSLocalizedString( - "We encountered an error loading plan information", comment: "Error message displayed when " + - "we're unable to fetch In-App Purchases plans from the server.") - - static let maximumSitesUpgradedErrorMessage = NSLocalizedString( - "A WooCommerce app store subscription with your Apple ID already exists", - comment: "Error message displayed when the merchant already has one store upgraded under the same Apple ID.") - - static let maximumSitesUpgradedErrorSubtitle = NSLocalizedString( - "An Apple ID can only be used to upgrade one store", - comment: "Subtitle message displayed when the merchant already has one store upgraded under the same Apple ID.") - - static let cancelUpgradeButtonText = NSLocalizedString( - "Cancel Upgrade", - comment: "Title of the button displayed when purchasing a plan fails, so the flow can be cancelled.") - - static let inAppPurchasesNotSupportedErrorMessage = NSLocalizedString( - "In-App Purchases not supported", - comment: "Error message displayed when In-App Purchases are not supported.") - - static let inAppPurchasesNotSupportedErrorSubtitle = NSLocalizedString( - "Please contact support for assistance.", - comment: "Subtitle message displayed when In-App Purchases are not supported, redirecting to contact support if needed.") - - static let unableToUpgradeText = NSLocalizedString( - "You can’t upgrade because you are not the store owner", - comment: "Text describing that is not possible to upgrade the site's plan.") - - static let unableToUpgradeInstructions = NSLocalizedString( - "Please contact the store owner to upgrade your plan.", - comment: "Text describing that only the site owner can upgrade the site's plan.") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/PurchaseUpgradeErrorView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/PurchaseUpgradeErrorView.swift deleted file mode 100644 index d91a276b4d5..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/PurchaseUpgradeErrorView.swift +++ /dev/null @@ -1,215 +0,0 @@ -import SwiftUI - -struct PurchaseUpgradeErrorView: View { - let error: PurchaseUpgradeError - let primaryAction: (() -> Void)? - let secondaryAction: (() -> Void) - let getSupportAction: (() -> Void) - - var body: some View { - VStack { - ScrollView(.vertical) { - VStack(alignment: .leading, spacing: Layout.spacing) { - Image(systemName: "exclamationmark.circle") - .font(.system(size: Layout.exclamationImageSize)) - .foregroundColor(.withColorStudio(name: .red, shade: .shade20)) - .accessibilityHidden(true) - VStack(alignment: .leading, spacing: Layout.textSpacing) { - Text(error.localizedTitle) - .font(.title) - .fontWeight(.bold) - Text(error.localizedDescription) - Text(error.localizedActionDirection) - .font(.title3) - .fontWeight(.bold) - if let actionHint = error.localizedActionHint { - Text(actionHint) - .font(.footnote) - } - if let errorCode = error.localizedErrorCode { - Text(String(format: Localization.errorCodeFormat, errorCode)) - .font(.footnote) - .foregroundColor(.secondary) - } - Button(action: getSupportAction) { - HStack { - Image(systemName: "questionmark.circle") - .font(.body.weight(.semibold)) - .foregroundColor(.withColorStudio(name: .blue, shade: .shade50)) - Text(Localization.getSupport) - .fontWeight(.semibold) - .foregroundColor(.withColorStudio(name: .blue, shade: .shade50)) - } - } - } - } - .padding(.top, Layout.topPadding) - .padding(.horizontal, Layout.horizontalPadding) - } - - Spacer() - - if let primaryButtonTitle = error.localizedPrimaryButtonLabel { - Button(primaryButtonTitle) { - primaryAction?() - } - .buttonStyle(PrimaryButtonStyle()) - .padding(.horizontal, Layout.horizontalPadding) - } - - Button(error.localizedSecondaryButtonTitle) { - secondaryAction() - } - .buttonStyle(SecondaryButtonStyle()) - .padding(.horizontal, Layout.horizontalPadding) - } - .padding(.bottom) - } -} - -private extension PurchaseUpgradeErrorView { - enum Layout { - static let exclamationImageSize: CGFloat = 56 - static let horizontalPadding: CGFloat = 16 - static let topPadding: CGFloat = 80 - static let spacing: CGFloat = 40 - static let textSpacing: CGFloat = 16 - } - - enum Localization { - static let getSupport = NSLocalizedString( - "Get support", - comment: "Button title to allow merchants to open the support screens when there's an error with their plan purchase") - static let errorCodeFormat = NSLocalizedString( - "Error code %1$@", - comment: "A string shown on the error screen when there's an issue purchasing a plan, to inform the user " + - "of the error code for use with Support. %1$@ will be replaced with the error code and must be included " + - "in the translations.") - } -} - -private extension PurchaseUpgradeError { - var localizedTitle: String { - switch self { - case .inAppPurchaseFailed: - return Localization.purchaseErrorTitle - case .planActivationFailed: - return Localization.activationErrorTitle - case .unknown: - return Localization.unknownErrorTitle - } - } - - var localizedDescription: String { - switch self { - case .inAppPurchaseFailed: - return Localization.purchaseErrorDescription - case .planActivationFailed: - return Localization.activationErrorDescription - case .unknown: - return Localization.unknownErrorDescription - } - } - - var localizedActionDirection: String { - switch self { - case .inAppPurchaseFailed: - return Localization.purchaseErrorActionDirection - case .planActivationFailed, .unknown: - return Localization.errorContactSupportActionDirection - } - } - - var localizedActionHint: String? { - switch self { - case .inAppPurchaseFailed: - return Localization.purchaseErrorActionHint - case .planActivationFailed: - return nil - case .unknown: - return nil - } - } - - var localizedErrorCode: String? { - switch self { - case .inAppPurchaseFailed(_, let underlyingError), .planActivationFailed(let underlyingError): - return underlyingError.errorCode - case .unknown: - return nil - } - } - - var localizedPrimaryButtonLabel: String? { - switch self { - case .inAppPurchaseFailed: - return Localization.retryPaymentButtonText - case .planActivationFailed: - return nil - case .unknown: - return nil - } - } - - var localizedSecondaryButtonTitle: String { - switch self { - case .inAppPurchaseFailed: - return Localization.cancelUpgradeButtonText - case .planActivationFailed, .unknown: - return Localization.returnToMyStoreButtonText - } - } - - private enum Localization { - /// Purchase errors - static let purchaseErrorTitle = NSLocalizedString( - "Error confirming payment", - comment: "Error message displayed when a payment fails when attempting to purchase a plan.") - - static let purchaseErrorDescription = NSLocalizedString( - "We encountered an error confirming your payment.", - comment: "Error description displayed when a payment fails when attempting to purchase a plan.") - - static let purchaseErrorActionDirection = NSLocalizedString( - "No payment has been taken", - comment: "Bolded message confirming that no payment has been taken when the upgrade failed.") - - static let purchaseErrorActionHint = NSLocalizedString( - "Please try again, or contact support for assistance", - comment: "Subtitle message displayed when the merchant already has one store upgraded under the same Apple ID.") - - static let retryPaymentButtonText = NSLocalizedString( - "Try Payment Again", - comment: "Title of the button displayed when purchasing a plan fails, so the merchant can try again.") - - static let cancelUpgradeButtonText = NSLocalizedString( - "Cancel Upgrade", - comment: "Title of the secondary button displayed when purchasing a plan fails, so the merchant can exit the flow.") - - /// Upgrade errors - static let activationErrorTitle = NSLocalizedString( - "Error activating plan", - comment: "Error message displayed when plan activation fails after purchasing a plan.") - - static let activationErrorDescription = NSLocalizedString( - "Your subscription is active, but there was an error activating the plan on your store.", - comment: "Error description displayed when plan activation fails after purchasing a plan.") - - static let errorContactSupportActionDirection = NSLocalizedString( - "Please contact support for assistance.", - comment: "Bolded message advising the merchant to contact support when the plan activation failed.") - - static let returnToMyStoreButtonText = NSLocalizedString( - "Return to My Store", - comment: "Title of the secondary button displayed when activating the purchased plan fails, so the merchant can exit the flow.") - - /// Unknown errors - static let unknownErrorTitle = NSLocalizedString( - "Error during purchase", - comment: "Title of an unknown error after purchasing a plan") - - static let unknownErrorDescription = NSLocalizedString( - "Something went wrong during your purchase, and we can't tell whether your payment has completed, or your store plan been upgraded.", - comment: "Description of an unknown error after purchasing a plan") - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/StorePlanSynchronizer.swift b/WooCommerce/Classes/ViewRelated/Upgrades/StorePlanSynchronizer.swift index 984adf0dddd..a70e4c6d749 100644 --- a/WooCommerce/Classes/ViewRelated/Upgrades/StorePlanSynchronizer.swift +++ b/WooCommerce/Classes/ViewRelated/Upgrades/StorePlanSynchronizer.swift @@ -5,6 +5,7 @@ import Experiments /// Protocol for used to mock `StorePlanSynchronizer`. /// +// periphery:ignore protocol StorePlanSynchronizing { /// Publisher for the current synced plan. var planStatePublisher: AnyPublisher { get } @@ -55,13 +56,8 @@ final class StorePlanSynchronizer: StorePlanSynchronizing { /// private var subscriptions: Set = [] - private let inAppPurchaseManager: InAppPurchasesForWPComPlansProtocol - - init(stores: StoresManager = ServiceLocator.stores, - pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager, - inAppPurchaseManager: InAppPurchasesForWPComPlansProtocol = InAppPurchasesForWPComPlansManager()) { + init(stores: StoresManager = ServiceLocator.stores) { self.stores = stores - self.inAppPurchaseManager = inAppPurchaseManager stores.site.sink { [weak self] site in guard let self else { return } diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsView.swift deleted file mode 100644 index ee7ef9aeb8d..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsView.swift +++ /dev/null @@ -1,125 +0,0 @@ -import Foundation -import SwiftUI - -/// Main view for the plan subscription settings. -/// -final class SubscriptionsHostingController: UIHostingController { - - init(siteID: Int64) { - let viewModel = SubscriptionsViewModel() - super.init(rootView: .init(viewModel: viewModel)) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -/// Main view for the plan settings. -/// -struct SubscriptionsView: View { - - /// Drives the view. - /// - @StateObject var viewModel: SubscriptionsViewModel - - /// Closure to be invoked when the "Report Issue" button is tapped. - /// - var onReportIssueTapped: (() -> ())? - - @State private var isShowingSupport = false - - var body: some View { - List { - Section(content: { - Text(Localization.currentPlan(viewModel.planName)) - .bodyStyle() - Button(action: { - viewModel.onManageSubscriptionButtonTapped() - }, label: { - Text(Localization.manageSubscription) - }) - .linkStyle() - .renderedIf(viewModel.shouldShowManageSubscriptionButton) - - }, header: { - Text(Localization.subscriptionStatus) - }, footer: { - Text(viewModel.planInfo) - }) - - Section(Localization.troubleshooting) { - Button(Localization.report) { - isShowingSupport = true - } - .linkStyle() - } - } - .notice($viewModel.errorNotice, autoDismiss: false) - .redacted(reason: viewModel.showLoadingIndicator ? .placeholder : []) - .shimmering(active: viewModel.showLoadingIndicator) - .background(Color(.listBackground)) - .navigationTitle(Localization.title) - .navigationBarTitleDisplayMode(.inline) - .task { - viewModel.loadPlan() - } - .sheet(isPresented: $isShowingSupport) { - supportForm - } - } -} - -private extension SubscriptionsView { - var supportForm: some View { - NavigationStack { - SupportForm(isPresented: $isShowingSupport, - viewModel: SupportFormViewModel()) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(Localization.done) { - isShowingSupport = false - } - } - } - } - } -} - -// Definitions -private extension SubscriptionsView { - enum Layout { - static let featureSpacing = 12.0 - static let sectionsSpacing = 24.0 - } - enum Localization { - static let title = NSLocalizedString("Subscriptions", comment: "Title for the Subscriptions / Upgrades view") - static let subscriptionStatus = NSLocalizedString("Subscription Status", comment: "Title for the plan section on the subscriptions view. Uppercased") - static let experienceFeatures = NSLocalizedString("Experience more of our features and services beyond the app", - comment: "Title for the features list in the Subscriptions Screen") - static let manageSubscription = NSLocalizedString("Manage Your Subscription", comment: "Title for the button to manage subscriptions") - static let troubleshooting = NSLocalizedString("Troubleshooting", - comment: "Title for the section to contact support on the subscriptions view. Uppercased") - static let report = NSLocalizedString("Report Subscription Issue", comment: "Title for the button to contact support on the Subscriptions view") - - static func currentPlan(_ plan: String) -> String { - let format = NSLocalizedString("Current: %@", comment: "Reads like: Current: Free Trial") - return .localizedStringWithFormat(format, plan) - } - - static let done = NSLocalizedString( - "subscriptionsView.dismissSupport", - value: "Done", - comment: "Button to dismiss the support form." - ) - } -} - -// MARK: Previews -struct UpgradesPreviews: PreviewProvider { - static var previews: some View { - NavigationView { - SubscriptionsView(viewModel: .init()) - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsViewModel.swift b/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsViewModel.swift deleted file mode 100644 index ac3a7b95cbd..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/SubscriptionsViewModel.swift +++ /dev/null @@ -1,292 +0,0 @@ -import Foundation -import UIKit -import Yosemite -import Combine -import protocol Experiments.FeatureFlagService -import protocol WooFoundation.Analytics - -/// ViewModel for the Subscriptions View -/// Drives the site's plan subscription -/// -final class SubscriptionsViewModel: ObservableObject { - - /// Indicates if the view should show an error notice. - /// - var errorNotice: Notice? = nil - - /// Current store plan. - /// - private(set) var planName = "" - - /// Current store plan details information. - /// - private(set) var planInfo = "" - - /// Current store plan details information. - /// - var planDaysLeft: Int? - - /// Current store plan expiration date, formatted as "MMMM d". e.g: "August 11" - /// - var formattedPlanExpirationDate: String? - - /// Defines if the view should show the "Manage Your Subscription" button. - /// - @Published private(set) var shouldShowManageSubscriptionButton = false - - /// Indicates if the view should should a redacted state. - /// - private(set) var showLoadingIndicator = false - - /// Observable subscription store. - /// - private var subscriptions: Set = [] - - /// Stores manager. - /// - private let stores: StoresManager - - /// Shared store plan synchronizer. - /// - private let storePlanSynchronizer: StorePlanSynchronizing - - /// Retrieves asynchronously all WPCom plans In-App Purchases products. - /// - private let inAppPurchasesPlanManager: InAppPurchasesForWPComPlansProtocol - - /// Analytics provider. - /// - private let analytics: Analytics - - /// Feature flag service. - /// - private let featureFlagService: FeatureFlagService - - init(stores: StoresManager = ServiceLocator.stores, - storePlanSynchronizer: StorePlanSynchronizing = ServiceLocator.storePlanSynchronizer, - inAppPurchasesPlanManager: InAppPurchasesForWPComPlansProtocol = InAppPurchasesForWPComPlansManager(), - analytics: Analytics = ServiceLocator.analytics, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { - self.stores = stores - self.storePlanSynchronizer = storePlanSynchronizer - self.inAppPurchasesPlanManager = inAppPurchasesPlanManager - self.analytics = analytics - self.featureFlagService = featureFlagService - observePlan() - } - - /// Loads the plan from network. - /// - func loadPlan() { - storePlanSynchronizer.reloadPlan() - } - - /// Opens the subscriptions management URL - /// - func onManageSubscriptionButtonTapped() { - let url = WooConstants.URLs.inAppPurchasesAccountSubscriptionsLink.asURL() - UIApplication.shared.open(url) - } -} - -// MARK: Helpers -private extension SubscriptionsViewModel { - /// Whether the In-App Purchases subscription management button should be rendered - /// - func shouldRenderManageSubscriptionsButton() { - guard let siteID = storePlanSynchronizer.site?.siteID else { - return - } - - Task { @MainActor in - if await inAppPurchasesPlanManager.siteHasCurrentInAppPurchases(siteID: siteID) { - self.shouldShowManageSubscriptionButton = true - } - } - } - - /// Observes and reacts to plan changes - /// - func observePlan() { - storePlanSynchronizer.planStatePublisher.sink { [weak self] planState in - guard let self else { return } - switch planState { - case .loading, .notLoaded: - self.updateLoadingViewProperties() - case .loaded(let plan): - self.updateViewProperties(from: plan) - case .expired: - self.updateExpiredViewProperties() - case .failed, .unavailable: - self.updateFailedViewProperties() - } - self.objectWillChange.send() - } - .store(in: &subscriptions) - } - - func updateViewProperties(from plan: WPComSitePlan) { - planName = getPlanName(from: plan) - planInfo = getPlanInfo(from: plan) - planDaysLeft = daysLeft(for: plan) - formattedPlanExpirationDate = formattedExpirationDate(for: plan) - errorNotice = nil - showLoadingIndicator = false - - if !plan.isFreeTrial { - shouldRenderManageSubscriptionsButton() - } - } - - func updateLoadingViewProperties() { - planName = "" - planInfo = "" - errorNotice = nil - showLoadingIndicator = true - } - - func updateExpiredViewProperties() { - planName = Localization.genericPlanEndedName - planInfo = Localization.planEndedInfo - errorNotice = nil - showLoadingIndicator = false - } - - func updateFailedViewProperties() { - planName = "" - planInfo = "" - errorNotice = createErrorNotice() - showLoadingIndicator = false - } - - /// Removes any occurrences of `WordPress.com` from the site's name. - /// Free Trial's have an special handling! - /// - func getPlanName(from plan: WPComSitePlan) -> String { - let daysLeft = daysLeft(for: plan) - if plan.isFreeTrial, daysLeft <= 0 { - return Localization.trialEnded - } - - let sanitizedName = WPComPlanNameSanitizer.getPlanName(from: plan) - if daysLeft > 0 { - return sanitizedName - } else { - return Localization.planEndedName(name: sanitizedName) - } - } - - /// Returns a plan specific details information. - /// - func getPlanInfo(from plan: WPComSitePlan) -> String { - let daysLeft = daysLeft(for: plan) - let planDuration = planDurationInDays(for: plan) - - if plan.isFreeTrial { - if daysLeft > 0 { - return Localization.freeTrialPlanInfo(planDuration: planDuration, daysLeft: daysLeft) - } else { - return Localization.trialEndedInfo - } - } - - let planName = getPlanName(from: plan) - guard let expireDate = plan.expiryDate else { - return "" - } - - guard daysLeft > 0 else { - return Localization.planEndedInfo - } - - let expireText = DateFormatter.mediumLengthLocalizedDateFormatter.string(from: expireDate) - return Localization.planInfo(planName: planName, expirationDate: expireText) - } - - /// Returns a site plan duration in days. - /// - func planDurationInDays(for plan: WPComSitePlan) -> Int { - // Normalize dates in the same timezone. - guard let subscribedDate = plan.subscribedDate?.startOfDay(timezone: .current), - let expiryDate = plan.expiryDate?.startOfDay(timezone: .current) else { - return 0 - } - - let duration = Calendar.current.dateComponents([.day], from: subscribedDate, to: expiryDate).day ?? 0 - return duration - } - - /// Returns the site plan expiration date formatted as "MMMM d". e.g: "August 11", - /// or nil if expired or we can't retrieve the expiration date - /// - func formattedExpirationDate(for plan: WPComSitePlan) -> String? { - guard daysLeft(for: plan) >= 0 else { - return nil - } - guard let planExpiryDate = plan.expiryDate else { - return nil - } - let dateFormatter = DateFormatter() - dateFormatter.setLocalizedDateFormatFromTemplate("MMMM d") - return dateFormatter.string(from: planExpiryDate) - } - - /// Returns how many days site plan has left. - /// - func daysLeft(for plan: WPComSitePlan) -> Int { - // Normalize dates in the same timezone. - let today = Date().startOfDay(timezone: .current) - guard let expiryDate = plan.expiryDate?.startOfDay(timezone: .current) else { - return 0 - } - - let daysLeft = Calendar.current.dateComponents([.day], from: today, to: expiryDate).day ?? 0 - return daysLeft - } - - /// Creates an error notice that allows to retry fetching a plan. - /// - func createErrorNotice() -> Notice { - .init(title: Localization.fetchErrorNotice, feedbackType: .error, actionTitle: Localization.retry) { [weak self] in - self?.loadPlan() - } - } -} - -// MARK: Definitions -private extension SubscriptionsViewModel { - enum Localization { - static let trialEnded = NSLocalizedString("Trial ended", comment: "Plan name for an expired free trial") - static let trialEndedInfo = NSLocalizedString("Your free trial has ended and you have limited access to all the features. " + - "Subscribe to a Woo Express Plan now.", - comment: "Info details for an expired free trial") - static let planEndedInfo = NSLocalizedString("Your subscription has ended and you have limited access to all the features.", - comment: "Info details for an expired plan") - static let fetchErrorNotice = NSLocalizedString("There was an error fetching your plan details, please try again later.", - comment: "Error shown when failing to fetch the plan details in the upgrades view.") - static let retry = NSLocalizedString("Retry", comment: "Retry button on the error notice for the upgrade view") - - static let genericPlanEndedName = NSLocalizedString( - "plan ended", - comment: "Shown with a 'Current:' label, but when we don't know what the plan that ended was") - - static func planEndedName(name: String) -> String { - let format = NSLocalizedString("%@ ended", comment: "Reads like: eCommerce ended") - return String.localizedStringWithFormat(format, name) - } - - static func freeTrialPlanInfo(planDuration: Int, daysLeft: Int) -> String { - let format = NSLocalizedString("You are in the %1$d-day free trial. The free trial will end in %2$d days. ", - comment: "Reads like: You are in the 14-day free trial. The free trial will end in 5 days. " + - "Upgrade to unlock new features and keep your store running.") - return String.localizedStringWithFormat(format, planDuration, daysLeft) - } - - static func planInfo(planName: String, expirationDate: String) -> String { - let format = NSLocalizedString("You are subscribed to the %1@ plan! You have access to all our features until %2@.", - comment: "Reads like: You are subscribed to the eCommerce plan! " + - "You have access to all our features until Nov 28, 2023.") - return String.localizedStringWithFormat(format, planName, expirationDate) - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeTopBarView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeTopBarView.swift deleted file mode 100644 index db745ca64b5..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeTopBarView.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI - -struct UpgradeTopBarView: View { - let dismiss: () -> Void - - var body: some View { - HStack { - Spacer() - - Text(Localization.navigationTitle) - .fontWeight(.bold) - .padding() - .frame(maxWidth: .infinity, alignment: .center) - .accessibilityAddTraits(.isHeader) - - Spacer() - } - .background(Color(.systemGroupedBackground)) - .overlay(alignment: .leading) { - Button(action: dismiss) { - Image(systemName: "xmark") - .font(.system(size: Layout.closeButtonSize)) - .foregroundColor(Color(.label)) - .padding() - .frame(alignment: .leading) - } - } - } -} - -private extension UpgradeTopBarView { - enum Localization { - static let navigationTitle = NSLocalizedString("Upgrade", comment: "Navigation title for the Upgrades screen") - } - - enum Layout { - static let closeButtonSize: CGFloat = 16 - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeWaitingView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeWaitingView.swift deleted file mode 100644 index a769a88c122..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradeWaitingView.swift +++ /dev/null @@ -1,51 +0,0 @@ -import SwiftUI -import struct WooFoundation.IndefiniteCircularProgressViewStyle - -struct UpgradeWaitingView: View { - let planName: String - - var body: some View { - VStack { - VStack(alignment: .leading, spacing: Layout.spacing) { - ProgressView() - .progressViewStyle(IndefiniteCircularProgressViewStyle(size: Layout.progressIndicatorSize, - lineWidth: Layout.progressIndicatorLineWidth)) - VStack(alignment: .leading, spacing: Layout.textSpacing) { - Text(Localization.title) - .font(.largeTitle) - .fontWeight(.bold) - .fixedSize(horizontal: false, vertical: true) - Text(String(format: Localization.descriptionFormatString, planName)) - .fixedSize(horizontal: false, vertical: true) - } - } - .padding(.horizontal, Layout.horizontalPadding) - .padding(.vertical, Layout.verticalPadding) - - Spacer() - } - } -} - -private extension UpgradeWaitingView { - enum Localization { - static let title = NSLocalizedString("You’re almost there", - comment: "Title for the progress screen shown after an In-App Purchase " + - "for a Woo Express plan, while we upgrade the site.") - - static let descriptionFormatString = NSLocalizedString( - "Please bear with us while we process the payment for your %1$@ plan.", - comment: "Detail text shown after an In-App Purchase for a Woo Express plan, shown while we upgrade the " + - "site. %1$@ is replaced with the short plan name. " + - "Reads as: 'Please bear with us while we process the payment for your Essential plan.'") - } - - enum Layout { - static let progressIndicatorSize: CGFloat = 56 - static let progressIndicatorLineWidth: CGFloat = 6 - static let horizontalPadding: CGFloat = 16 - static let verticalPadding: CGFloat = 152 - static let spacing: CGFloat = 40 - static let textSpacing: CGFloat = 16 - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradesView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/UpgradesView.swift deleted file mode 100644 index 9eab08a9219..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/UpgradesView.swift +++ /dev/null @@ -1,212 +0,0 @@ -import Foundation -import SwiftUI -import Yosemite -import Experiments - -final class UpgradesViewPresentationCoordinator { - private let featureFlagService: FeatureFlagService - private let inAppPurchaseManager: InAppPurchasesForWPComPlansProtocol - - init(featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - inAppPurchaseManager: InAppPurchasesForWPComPlansProtocol = InAppPurchasesForWPComPlansManager()) { - self.featureFlagService = featureFlagService - self.inAppPurchaseManager = inAppPurchaseManager - } - - func presentUpgrades(for siteID: Int64, from viewController: UIViewController) { - Task { @MainActor in - if await inAppPurchaseManager.inAppPurchasesAreSupported() { - let upgradesController = UpgradesHostingController(siteID: siteID) - viewController.present(upgradesController, animated: true) - } else { - let subscriptionsController = SubscriptionsHostingController(siteID: siteID) - viewController.present(subscriptionsController, animated: true) - } - } - } -} - -/// Hosting controller for `UpgradesView` -/// To be used to display available current plan Subscriptions, available plan Upgrades, -/// and the CTA to upgrade -/// -final class UpgradesHostingController: UIHostingController { - private let authentication: Authentication = ServiceLocator.authenticationManager - - init(siteID: Int64) { - let upgradesViewModel = UpgradesViewModel(siteID: siteID) - let subscriptionsViewModel = SubscriptionsViewModel() - - super.init(rootView: UpgradesView(upgradesViewModel: upgradesViewModel, subscriptionsViewModel: subscriptionsViewModel)) - - rootView.supportHandler = { [weak self] in - self?.openSupport() - } - } - - func openSupport() { - authentication.presentSupport(from: self, screen: .purchasePlanError) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("Not implemented") - } -} - -struct UpgradesView: View { - @Environment(\.dismiss) var dismiss - - @ObservedObject var upgradesViewModel: UpgradesViewModel - @ObservedObject var subscriptionsViewModel: SubscriptionsViewModel - - var supportHandler: () -> Void = {} - - init(upgradesViewModel: UpgradesViewModel, - subscriptionsViewModel: SubscriptionsViewModel) { - self.upgradesViewModel = upgradesViewModel - self.subscriptionsViewModel = subscriptionsViewModel - } - - var body: some View { - NavigationStack { - VStack { - VStack { - // TODO: Once we remove iOS 15 support, we can do this with .toolbar instead. - UpgradeTopBarView(dismiss: { - dismiss() - }) - } - - switch upgradesViewModel.upgradeViewState { - case .loading: - OwnerUpgradesView(upgradePlans: [ - .skeletonPlan(frequency: .year, shortName: "Essential"), - .skeletonPlan(frequency: .year, shortName: "Performance"), - .skeletonPlan(frequency: .month, shortName: "Essential"), - .skeletonPlan(frequency: .month, shortName: "Performance")], - isPurchasing: .constant(false), - expirationDate: .constant(""), - planDaysLeft: .constant(0), - purchasePlanAction: { _ in }, isLoading: true) - .accessibilityLabel(Localization.plansLoadingAccessibilityLabel) - case .loaded(let plans): - OwnerUpgradesView(upgradePlans: plans, - isPurchasing: $upgradesViewModel.isPurchasing, - expirationDate: $subscriptionsViewModel.formattedPlanExpirationDate, - planDaysLeft: $subscriptionsViewModel.planDaysLeft, - purchasePlanAction: { selectedPlan in - Task { - await upgradesViewModel.purchasePlan(with: selectedPlan.wpComPlan.id) - } - }) - case .waiting(let plan): - ScrollView(.vertical) { - UpgradeWaitingView(planName: plan.wooPlan.shortName) - } - case .completed(let plan): - CompletedUpgradeView(planName: plan.wooPlan.shortName, - doneAction: { - dismiss() - }) - case .prePurchaseError(let error): - ScrollView(.vertical) { - VStack { - PrePurchaseUpgradesErrorView(error, - onRetryButtonTapped: { - upgradesViewModel.retryFetch() - }) - .padding(.top, Layout.errorViewTopPadding) - .padding(.horizontal, Layout.errorViewHorizontalPadding) - - Spacer() - } - } - .background(Color(.systemGroupedBackground)) - case .purchaseUpgradeError(.inAppPurchaseFailed(let plan, let iapStoreError)): - PurchaseUpgradeErrorView(error: .inAppPurchaseFailed(plan, iapStoreError)) { - Task { - await upgradesViewModel.purchasePlan(with: plan.wpComPlan.id) - } - } secondaryAction: { - dismiss() - } getSupportAction: { - supportHandler() - } - case .purchaseUpgradeError(let underlyingError): - // handles .planActivationFailed and .unknown underlyingErrors - PurchaseUpgradeErrorView(error: underlyingError, - primaryAction: nil, - secondaryAction: { - dismiss() - }, - getSupportAction: supportHandler) - } - } - .navigationBarHidden(true) - .background(Color(.systemGroupedBackground)) - } - .onDisappear { - upgradesViewModel.onDisappear() - } - .task { - await upgradesViewModel.prepareViewModel() - } - } -} - -private extension UpgradesView { - struct Layout { - static let errorViewHorizontalPadding: CGFloat = 20 - static let errorViewTopPadding: CGFloat = 36 - static let padding: CGFloat = 16 - static let contentSpacing: CGFloat = 8 - static let smallPadding: CGFloat = 8 - } - - enum Localization { - static let plansLoadingAccessibilityLabel = NSLocalizedString( - "Loading plan details", - comment: "Accessibility label for the initial loading state of the Upgrades view") - } -} - -private extension WooWPComPlan { - static func skeletonPlan(frequency: WooPlan.PlanFrequency, shortName: String) -> WooWPComPlan { - let planProduct = SkeletonWPComPlanProduct(displayName: "\(frequency.localizedPlanName) \(shortName) Plan", - id: "skeleton.wpcom.plan.product.monthly", - price: "$100") - return WooWPComPlan( - wpComPlan: planProduct, - wooPlan: WooPlan(id: "skeleton.plan.\(shortName).\(frequency.rawValue)", - name: "Skeleton \(shortName) Plan \(frequency.localizedPlanName)", - shortName: "Skeleton", - planFrequency: frequency, - planDescription: "A skeleton plan to show (redacted) while we're loading", - planFeatures: []), - hardcodedPlanDataIsValid: true) - } - - private struct SkeletonWPComPlanProduct: WPComPlanProduct { - let displayName: String - let description: String = "A skeleton plan to show (redacted) while we're loading" - let id: String - let displayPrice: String - - init(displayName: String, - id: String, - price: String) { - self.displayName = displayName - self.id = id - self.displayPrice = price - } - - - } -} - -struct UpgradesView_Preview: PreviewProvider { - static var previews: some View { - UpgradesView(upgradesViewModel: UpgradesViewModel(siteID: 0), - subscriptionsViewModel: SubscriptionsViewModel()) - } -} diff --git a/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift b/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift deleted file mode 100644 index 7bca18cca0b..00000000000 --- a/WooCommerce/Classes/ViewRelated/Upgrades/WooPlanCardView.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Yosemite -import SwiftUI - -struct WooPlanCardView: View { - let upgradePlan: WooWPComPlan - @Binding var selectedPlan: WooWPComPlan? - - private var isSelected: Bool { - selectedPlan?.id == upgradePlan.id - } - - @State private var isExpanded = false - - var body: some View { - VStack(alignment: .leading, spacing: Layout.spacing) { - VStack(alignment: .leading, spacing: Layout.spacing) { - HStack { - Text(upgradePlan.wooPlan.shortName) - .font(.title2) - .bold() - - Spacer() - - BadgeView(text: Localization.isPopularBadgeText.uppercased()) - .renderedIf(upgradePlan.shouldDisplayIsPopularBadge) - Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") - .foregroundStyle(isSelected ? Color.withColorStudio(name: .wooCommercePurple, shade: .shade50) : Color(.systemGray4)) - .font(.system(size: Layout.checkImageSize)) - } - .accessibilityElement() - .accessibilityLabel(upgradePlan.wooPlan.shortName) - .accessibilityAddTraits([.isHeader, .isButton]) - .accessibilityAddTraits(isSelected ? [.isSelected] : []) - - Text(upgradePlan.wooPlan.planDescription) - .font(.subheadline) - } - - VStack(alignment: .leading, spacing: Layout.textSpacing) { - Text(upgradePlan.wpComPlan.displayPrice) - .font(.title) - .bold() - .accessibilityAddTraits(.isHeader) - Text(upgradePlan.wooPlan.planFrequency.localizedString) - .font(.footnote) - } - - let buttonText = String.localizedStringWithFormat(Localization.viewPlanFeaturesFormat, upgradePlan.wooPlan.shortName) - Button("\(buttonText) \(Image(systemName: isExpanded ? "chevron.up" : "chevron.down"))") { - isExpanded.toggle() - } - WooPlanCardFeaturesView(upgradePlan).renderedIf(isExpanded) - } - .frame(maxWidth: .infinity) - .padding(.horizontal) - .padding(.vertical) - .background(Color(.secondarySystemGroupedBackground)) - .cornerRadius(Layout.cornerRadius) - .overlay( - RoundedRectangle(cornerRadius: Layout.cornerRadius) - .stroke(isSelected ? .withColorStudio(name: .wooCommercePurple, shade: .shade50) : Color(.systemGray4), - lineWidth: isSelected ? Layout.selectedBorder : Layout.unselectedBorder) - ) - .contentShape(Rectangle()) - .onTapGesture { - if selectedPlan?.id != upgradePlan.id { - selectedPlan = upgradePlan - } - } - } -} - -private struct WooPlanCardFeaturesView: View { - private let plan: WooWPComPlan - - init(_ plan: WooWPComPlan) { - self.plan = plan - } - - var planFeatures: [String] { - plan.wooPlan.planFeatures - } - - var body: some View { - VStack(alignment: .leading, spacing: Layout.featureTextSpacing) { - Text(Localization.upsellFeatureTitleText) - .bold() - .font(.footnote) - .renderedIf(!plan.wooPlan.isEssential) - ForEach(planFeatures, id: \.self) { feature in - Text(feature) - .font(.footnote) - .multilineTextAlignment(.leading) - } - Text(Localization.storageText.uppercased()) - .bold() - .font(.footnote) - BadgeView(text: Localization.storageAmountText, customizations: .init(textColor: Color(.text), backgroundColor: Color(.systemGray4))) - .font(.footnote) - } - } -} - -private extension WooPlanCardFeaturesView { - enum Localization { - static let upsellFeatureTitleText = NSLocalizedString("Everything in Essential, plus:", - comment: "Title for the Performance plan features list." + - " Is followed by a list of the plan features.") - - static let storageText = NSLocalizedString("Storage", - comment: "Title of one of the features of the Paid plans, regarding site storage.") - - static let storageAmountText = NSLocalizedString("50 GB", - comment: "Content of one of the features of the Paid plans, pointing to gigabytes of site storage.") - } - - enum Layout { - static let featureTextSpacing: CGFloat = 8.0 - } -} - -private extension WooPlanCardView { - enum Layout { - static let cornerRadius: CGFloat = 8.0 - static let selectedBorder: CGFloat = 2 - static let unselectedBorder: CGFloat = 0.5 - static let checkImageSize: CGFloat = 24 - static let spacing: CGFloat = 16 - static let textSpacing: CGFloat = 4 - } - - enum Localization { - static let viewPlanFeaturesFormat = NSLocalizedString( - "View %1$@ features", - comment: "Title for the button to expand plan details on the Upgrade plan screen. " + - "Reads as 'View Essential features'. %1$@ must be included in the string and will be replaced with " + - "the plan name.") - - static let isPopularBadgeText = NSLocalizedString( - "Popular", - comment: "The text of the badge that indicates the most popular choice when purchasing a Plan") - } -} diff --git a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift index 97e2f60286a..8efc471b60a 100644 --- a/WooCommerce/Classes/Yosemite/AuthenticatedState.swift +++ b/WooCommerce/Classes/Yosemite/AuthenticatedState.swift @@ -69,7 +69,6 @@ class AuthenticatedState: StoresManagerState { CustomerStore(dispatcher: dispatcher, storageManager: storageManager, network: network), DataStore(dispatcher: dispatcher, storageManager: storageManager, network: network), FeatureFlagStore(dispatcher: dispatcher, storageManager: storageManager, network: network), - InAppPurchaseStore(dispatcher: dispatcher, storageManager: storageManager, network: network), InboxNotesStore(dispatcher: dispatcher, storageManager: storageManager, network: network), JetpackSettingsStore(dispatcher: dispatcher, storageManager: storageManager, network: network), JustInTimeMessageStore(dispatcher: dispatcher, storageManager: storageManager, network: network), diff --git a/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/Contents.json b/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/Contents.json deleted file mode 100644 index d577e7f9225..00000000000 --- a/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "cart-filled.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/cart-filled.pdf b/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/cart-filled.pdf deleted file mode 100644 index b7fb98ee7c7..00000000000 Binary files a/WooCommerce/Resources/Images.xcassets/icon-shopping-cart-filled.imageset/cart-filled.pdf and /dev/null differ diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index d425966aa45..82e05a13087 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -591,7 +591,6 @@ 03191AE928E20C9200670723 /* PluginDetailsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03191AE828E20C9200670723 /* PluginDetailsRowView.swift */; }; 031B10E3274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031B10E2274FE2AE007390BA /* CardPresentModalConnectionFailedUpdateAddress.swift */; }; 032E481D2982996E00469D92 /* CardPresentModalTapToPayConnectingFailedNonRetryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E481C2982996E00469D92 /* CardPresentModalTapToPayConnectingFailedNonRetryable.swift */; }; - 0331A7002A334982001D2C2C /* MockInAppPurchasesForWPComPlansManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02863F6E2925FC29006A06AA /* MockInAppPurchasesForWPComPlansManager.swift */; }; 03582BE2299A9CC8007B7AA3 /* CollectOrderPaymentAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03582BE1299A9CC8007B7AA3 /* CollectOrderPaymentAnalytics.swift */; }; 035BA3A8291000E90056F0AD /* JustInTimeMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035BA3A7291000E90056F0AD /* JustInTimeMessageViewModelTests.swift */; }; 035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035C6DEA273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift */; }; @@ -773,10 +772,7 @@ 261AA30E275506DE009530FE /* PaymentMethodsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261AA30D275506DE009530FE /* PaymentMethodsViewModelTests.swift */; }; 261B526E29B795DB00DF7AB6 /* SupportFormMetadataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261B526D29B795DB00DF7AB6 /* SupportFormMetadataProvider.swift */; }; 261C21552C08D8E20051AF19 /* WatchTracksProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261C21542C08D8E20051AF19 /* WatchTracksProvider.swift */; }; - 261E91A029C961EE00A5C118 /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261E919F29C961EE00A5C118 /* SubscriptionsViewModel.swift */; }; - 261E91A329C9882600A5C118 /* SubscriptionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261E91A229C9882600A5C118 /* SubscriptionsViewModelTests.swift */; }; 261F1A7929C2AB2E001D9861 /* FreeTrialBannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261F1A7829C2AB2E001D9861 /* FreeTrialBannerViewModel.swift */; }; - 261F1A7C29C2B09D001D9861 /* FreeTrialBannerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261F1A7B29C2B09D001D9861 /* FreeTrialBannerViewModelTests.swift */; }; 262418332B8D3630009A3834 /* ApplicationPasswordTutorial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262418322B8D3630009A3834 /* ApplicationPasswordTutorial.swift */; }; 262418372B9044FF009A3834 /* ApplicationPasswordTutorialViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262418362B9044FF009A3834 /* ApplicationPasswordTutorialViewModel.swift */; }; 262562352C52A6410075A8CC /* WooAnalyticsEvent+BackgroudUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262562342C52A6410075A8CC /* WooAnalyticsEvent+BackgroudUpdates.swift */; }; @@ -797,7 +793,6 @@ 26309F17277D0AEA0012797F /* SafeAreaInsetsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26309F16277D0AEA0012797F /* SafeAreaInsetsKey.swift */; }; 2631D4F829ED0B5C00F13F20 /* WPComPlanNameSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2631D4F729ED0B5C00F13F20 /* WPComPlanNameSanitizer.swift */; }; 2631D4FA29ED108400F13F20 /* WPComPlanNameSanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2631D4F929ED108400F13F20 /* WPComPlanNameSanitizer.swift */; }; - 2631D4FE29F2141D00F13F20 /* StorePlanBannerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2631D4FD29F2141D00F13F20 /* StorePlanBannerPresenter.swift */; }; 263491D5299C923400594566 /* SupportFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263491D4299C923300594566 /* SupportFormViewModelTests.swift */; }; 26373E2C2BFD13E0008E6735 /* OrdersDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26373E2B2BFD13E0008E6735 /* OrdersDataService.swift */; }; 26373E2E2BFD2F46008E6735 /* OrdersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26373E2D2BFD2F46008E6735 /* OrdersListViewModel.swift */; }; @@ -807,7 +802,6 @@ 263EB409242C58EA00F3A15F /* ProductFormActionsFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263EB408242C58EA00F3A15F /* ProductFormActionsFactoryTests.swift */; }; 2647F7B529280A7F00D59FDF /* AnalyticsHubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2647F7B429280A7F00D59FDF /* AnalyticsHubView.swift */; }; 2647F7BA292BE2F900D59FDF /* AnalyticsReportCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2647F7B9292BE2F900D59FDF /* AnalyticsReportCard.swift */; }; - 264957A329C520860095AA4C /* SubscriptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264957A229C520860095AA4C /* SubscriptionsView.swift */; }; 264E9E942BF1D1DF009C48FD /* AppLocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F50FE4228CAEBA800C89201 /* AppLocalizedString.swift */; }; 264E9E952BF400AD009C48FD /* StoreInfoDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260DE20828CA7CE2009ECD7C /* StoreInfoDataService.swift */; }; 265284022624937600F91BA1 /* AddOnCrossreferenceUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265284012624937600F91BA1 /* AddOnCrossreferenceUseCase.swift */; }; @@ -1347,7 +1341,6 @@ 640DA3482E97DE4F00317FB2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640DA3472E97DE4F00317FB2 /* SceneDelegate.swift */; }; 64D355A52E99048E005F53F7 /* TestingSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D355A42E99048E005F53F7 /* TestingSceneDelegate.swift */; }; 68051E1E2E9DFE5500228196 /* POSNotificationSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68051E1D2E9DFE5100228196 /* POSNotificationSchedulerTests.swift */; }; - 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680BA5992A4C377900F5559D /* UpgradeViewState.swift */; }; 680E36B52BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 680E36B42BD8B9B900E8BCEA /* OrderSubscriptionTableViewCell.xib */; }; 680E36B72BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680E36B62BD8C49F00E8BCEA /* OrderSubscriptionTableViewCellViewModel.swift */; }; 682140AF2E125437005E86AB /* UILabel+SalesChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682140AE2E125430005E86AB /* UILabel+SalesChannel.swift */; }; @@ -1356,7 +1349,6 @@ 6832C7CA26DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */; }; 6832C7CC26DA5FDF00BA4088 /* LabeledTextViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */; }; 683421642ACE9391009021D7 /* ProductDiscountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683421632ACE9391009021D7 /* ProductDiscountView.swift */; }; - 683AA9D62A303CB70099F7BA /* UpgradesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683AA9D52A303CB70099F7BA /* UpgradesViewModelTests.swift */; }; 683F18662E9CC839007BC608 /* POSNotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */; }; 684AB83A2870677F003DFDD1 /* CardReaderManualsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684AB8392870677F003DFDD1 /* CardReaderManualsView.swift */; }; 684AB83C2873DF04003DFDD1 /* CardReaderManualsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684AB83B2873DF04003DFDD1 /* CardReaderManualsViewModel.swift */; }; @@ -1372,13 +1364,8 @@ 6856DE479EC3B2265AC1F775 /* Calendar+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D66A1963092C34D20674 /* Calendar+Extensions.swift */; }; 6856DF20E1BDCC391635F707 /* AgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D1A5F72A36AB3704D19D /* AgeTests.swift */; }; 68674D312B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68674D302B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift */; }; - 68709D3D2A2ED94900A7FA6C /* UpgradesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68709D3C2A2ED94900A7FA6C /* UpgradesView.swift */; }; - 68709D402A2EE2DC00A7FA6C /* UpgradesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68709D3F2A2EE2DC00A7FA6C /* UpgradesViewModel.swift */; }; 6879B8DB287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */; }; 687C006F2D6346E300F832FC /* POSCollectOrderPaymentAnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 687C006E2D6346E300F832FC /* POSCollectOrderPaymentAnalyticsTests.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 */; }; 68A38DF52B293B030090C263 /* MockProductListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A38DF42B293B030090C263 /* MockProductListViewModel.swift */; }; 68A5221B2BA1804900A6A584 /* PluginDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A5221A2BA1804900A6A584 /* PluginDetailsViewModelTests.swift */; }; 68A905012ACCFC13004C71D3 /* CollapsibleProductCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68A905002ACCFC13004C71D3 /* CollapsibleProductCard.swift */; }; @@ -1391,15 +1378,6 @@ 68D5094E2AD39BC900B6FFD5 /* DiscountLineDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D5094D2AD39BC900B6FFD5 /* DiscountLineDetailsView.swift */; }; 68DF5A8D2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8C2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift */; }; 68DF5A8F2CB38F20000154C9 /* OrderCouponSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68DF5A8E2CB38F20000154C9 /* OrderCouponSectionView.swift */; }; - 68E6749F2A4DA01C0034BA1E /* WooWPComPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */; }; - 68E674A12A4DA0B30034BA1E /* InAppPurchasesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A02A4DA0B30034BA1E /* InAppPurchasesError.swift */; }; - 68E674A32A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A22A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift */; }; - 68E674A52A4DA8510034BA1E /* PurchaseUpgradeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A42A4DA8510034BA1E /* PurchaseUpgradeErrorView.swift */; }; - 68E674A72A4DAAA60034BA1E /* UpgradeWaitingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A62A4DAAA60034BA1E /* UpgradeWaitingView.swift */; }; - 68E674A92A4DAB1A0034BA1E /* OwnerUpgradesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674A82A4DAB1A0034BA1E /* OwnerUpgradesView.swift */; }; - 68E674AB2A4DAB8C0034BA1E /* CompletedUpgradeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674AA2A4DAB8C0034BA1E /* CompletedUpgradeView.swift */; }; - 68E674AD2A4DAC010034BA1E /* CurrentPlanDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674AC2A4DAC010034BA1E /* CurrentPlanDetailsView.swift */; }; - 68E674AF2A4DACD50034BA1E /* UpgradeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */; }; 68E952D0287587BF0095A23D /* CardReaderManualRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E952CF287587BF0095A23D /* CardReaderManualRowView.swift */; }; 68E952D22875A44B0095A23D /* CardReaderType+Manual.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68E952D12875A44B0095A23D /* CardReaderType+Manual.swift */; }; 68ED2BD62ADD2C8C00ECA88D /* LineDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68ED2BD52ADD2C8C00ECA88D /* LineDetailView.swift */; }; @@ -1814,7 +1792,6 @@ B95A45EC2A77D7A60073A91F /* CustomerSelectorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95A45EB2A77D7A60073A91F /* CustomerSelectorViewModelTests.swift */; }; B95B15C92B15EBA000A54044 /* UpdateProductInventoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B95B15C82B15EBA000A54044 /* UpdateProductInventoryViewModel.swift */; }; B96B536B2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96B536A2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift */; }; - B96D6C0729081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */; }; B976D5BB2D3808A000D01E2E /* WooShippingCustomsFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B976D5BA2D38089C00D01E2E /* WooShippingCustomsFormViewModelTests.swift */; }; B97C6E562B15E51A008A2BF2 /* UpdateProductInventoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97C6E552B15E51A008A2BF2 /* UpdateProductInventoryView.swift */; }; B98968572A98F227007A2FBE /* TaxEducationalDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98968562A98F227007A2FBE /* TaxEducationalDialogViewModelTests.swift */; }; @@ -2523,7 +2500,6 @@ DEB387A32C36474F0025256E /* GoogleAdsDashboardCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEB387A22C36474F0025256E /* GoogleAdsDashboardCard.swift */; }; DEB387A52C36479B0025256E /* GoogleAdsDashboardCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEB387A42C36479B0025256E /* GoogleAdsDashboardCardViewModel.swift */; }; DEB387A72C3682C40025256E /* GoogleAdsCampaign+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEB387A62C3682C40025256E /* GoogleAdsCampaign+UI.swift */; }; - DEBAB70B2A7A3FE000743185 /* StorePlanBannerPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBAB70A2A7A3FE000743185 /* StorePlanBannerPresenterTests.swift */; }; DEBAB70D2A7A6F1100743185 /* MockStorePlanSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBAB70C2A7A6F1100743185 /* MockStorePlanSynchronizer.swift */; }; DEBAB70F2A7A6F3800743185 /* MockConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBAB70E2A7A6F3800743185 /* MockConnectivityObserver.swift */; }; DEC0293729C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */; }; @@ -2641,7 +2617,6 @@ E16715CD2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16715CC2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift */; }; E17E3BF9266917C10009D977 /* CardPresentModalScanningFailedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17E3BF8266917C10009D977 /* CardPresentModalScanningFailedTests.swift */; }; E17E3BFB266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17E3BFA266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift */; }; - E181CDCC291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */; }; E1ABAEF728479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ABAEF628479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift */; }; E1B0839B291BC5E3001D99C8 /* WooCommerceTest.storekit in Resources */ = {isa = PBXBuildFile; fileRef = E1B0839A291BC5DD001D99C8 /* WooCommerceTest.storekit */; }; E1BAAEA026BBECEF00F2C037 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAAE9F26BBECEF00F2C037 /* ButtonStyles.swift */; }; @@ -3299,7 +3274,6 @@ 0282DD93233C9465006A5FDB /* SearchUICommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUICommand.swift; sourceTree = ""; }; 0282DD95233C960C006A5FDB /* SearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCell.swift; sourceTree = ""; }; 0282DD97233CA093006A5FDB /* OrderSearchUICommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSearchUICommand.swift; sourceTree = ""; }; - 02863F6E2925FC29006A06AA /* MockInAppPurchasesForWPComPlansManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInAppPurchasesForWPComPlansManager.swift; sourceTree = ""; }; 0286837627B25930000E5785 /* HubMenuViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubMenuViewModelTests.swift; sourceTree = ""; }; 0286B27623C7051F003D784B /* ProductImagesCollectionViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProductImagesCollectionViewController.xib; sourceTree = ""; }; 0286B27723C7051F003D784B /* ProductImagesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductImagesCollectionViewController.swift; sourceTree = ""; }; @@ -3712,10 +3686,7 @@ 261AA30D275506DE009530FE /* PaymentMethodsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodsViewModelTests.swift; sourceTree = ""; }; 261B526D29B795DB00DF7AB6 /* SupportFormMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportFormMetadataProvider.swift; sourceTree = ""; }; 261C21542C08D8E20051AF19 /* WatchTracksProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchTracksProvider.swift; sourceTree = ""; }; - 261E919F29C961EE00A5C118 /* SubscriptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModel.swift; sourceTree = ""; }; - 261E91A229C9882600A5C118 /* SubscriptionsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModelTests.swift; sourceTree = ""; }; 261F1A7829C2AB2E001D9861 /* FreeTrialBannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeTrialBannerViewModel.swift; sourceTree = ""; }; - 261F1A7B29C2B09D001D9861 /* FreeTrialBannerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeTrialBannerViewModelTests.swift; sourceTree = ""; }; 262418322B8D3630009A3834 /* ApplicationPasswordTutorial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordTutorial.swift; sourceTree = ""; }; 262418362B9044FF009A3834 /* ApplicationPasswordTutorialViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordTutorialViewModel.swift; sourceTree = ""; }; 262562342C52A6410075A8CC /* WooAnalyticsEvent+BackgroudUpdates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+BackgroudUpdates.swift"; sourceTree = ""; }; @@ -3735,7 +3706,6 @@ 26309F16277D0AEA0012797F /* SafeAreaInsetsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaInsetsKey.swift; sourceTree = ""; }; 2631D4F729ED0B5C00F13F20 /* WPComPlanNameSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPComPlanNameSanitizer.swift; sourceTree = ""; }; 2631D4F929ED108400F13F20 /* WPComPlanNameSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPComPlanNameSanitizer.swift; sourceTree = ""; }; - 2631D4FD29F2141D00F13F20 /* StorePlanBannerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePlanBannerPresenter.swift; sourceTree = ""; }; 263491D4299C923300594566 /* SupportFormViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportFormViewModelTests.swift; sourceTree = ""; }; 26373E2B2BFD13E0008E6735 /* OrdersDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersDataService.swift; sourceTree = ""; }; 26373E2D2BFD2F46008E6735 /* OrdersListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersListViewModel.swift; sourceTree = ""; }; @@ -3746,7 +3716,6 @@ 263EB408242C58EA00F3A15F /* ProductFormActionsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormActionsFactoryTests.swift; sourceTree = ""; }; 2647F7B429280A7F00D59FDF /* AnalyticsHubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubView.swift; sourceTree = ""; }; 2647F7B9292BE2F900D59FDF /* AnalyticsReportCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsReportCard.swift; sourceTree = ""; }; - 264957A229C520860095AA4C /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = ""; }; 265284012624937600F91BA1 /* AddOnCrossreferenceUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOnCrossreferenceUseCase.swift; sourceTree = ""; }; 265284082624ACE900F91BA1 /* AddOnCrossreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOnCrossreferenceTests.swift; sourceTree = ""; }; 2655905A27863D1300BB8457 /* MockCollectOrderPaymentUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCollectOrderPaymentUseCase.swift; sourceTree = ""; }; @@ -4271,7 +4240,6 @@ 6832C7C926DA5C4500BA4088 /* LabeledTextViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledTextViewTableViewCell.swift; sourceTree = ""; }; 6832C7CB26DA5FDE00BA4088 /* LabeledTextViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LabeledTextViewTableViewCell.xib; sourceTree = ""; }; 683421632ACE9391009021D7 /* ProductDiscountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDiscountView.swift; sourceTree = ""; }; - 683AA9D52A303CB70099F7BA /* UpgradesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradesViewModelTests.swift; sourceTree = ""; }; 683F18652E9CC838007BC608 /* POSNotificationScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSNotificationScheduler.swift; sourceTree = ""; }; 684AB8392870677F003DFDD1 /* CardReaderManualsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsView.swift; sourceTree = ""; }; 684AB83B2873DF04003DFDD1 /* CardReaderManualsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsViewModel.swift; sourceTree = ""; }; @@ -4287,13 +4255,10 @@ 6856D7981E11F85D5E4EFED7 /* NSMutableAttributedStringHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSMutableAttributedStringHelperTests.swift; sourceTree = ""; }; 6856DCE1638958DA296D690F /* KeyboardStateProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardStateProviderTests.swift; sourceTree = ""; }; 68674D302B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEligibilityUseCaseTests.swift; sourceTree = ""; }; - 68709D3C2A2ED94900A7FA6C /* UpgradesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradesView.swift; sourceTree = ""; }; 68709D3F2A2EE2DC00A7FA6C /* UpgradesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradesViewModel.swift; sourceTree = ""; }; 6879B8DA287AFFA100A0F9A8 /* CardReaderManualsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualsViewModelTests.swift; sourceTree = ""; }; 687C006E2D6346E300F832FC /* POSCollectOrderPaymentAnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCollectOrderPaymentAnalyticsTests.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 = ""; }; 68A38DF42B293B030090C263 /* MockProductListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProductListViewModel.swift; sourceTree = ""; }; 68A5221A2BA1804900A6A584 /* PluginDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDetailsViewModelTests.swift; sourceTree = ""; }; 68A905002ACCFC13004C71D3 /* CollapsibleProductCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleProductCard.swift; sourceTree = ""; }; @@ -4309,11 +4274,7 @@ 68E6749E2A4DA01C0034BA1E /* WooWPComPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooWPComPlan.swift; sourceTree = ""; }; 68E674A02A4DA0B30034BA1E /* InAppPurchasesError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesError.swift; sourceTree = ""; }; 68E674A22A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrePurchaseUpgradesErrorView.swift; sourceTree = ""; }; - 68E674A42A4DA8510034BA1E /* PurchaseUpgradeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseUpgradeErrorView.swift; sourceTree = ""; }; - 68E674A62A4DAAA60034BA1E /* UpgradeWaitingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeWaitingView.swift; sourceTree = ""; }; - 68E674A82A4DAB1A0034BA1E /* OwnerUpgradesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnerUpgradesView.swift; sourceTree = ""; }; 68E674AA2A4DAB8C0034BA1E /* CompletedUpgradeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletedUpgradeView.swift; sourceTree = ""; }; - 68E674AC2A4DAC010034BA1E /* CurrentPlanDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentPlanDetailsView.swift; sourceTree = ""; }; 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeTopBarView.swift; sourceTree = ""; }; 68E952CF287587BF0095A23D /* CardReaderManualRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderManualRowView.swift; sourceTree = ""; }; 68E952D12875A44B0095A23D /* CardReaderType+Manual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardReaderType+Manual.swift"; sourceTree = ""; }; @@ -4759,7 +4720,6 @@ B95A45EB2A77D7A60073A91F /* CustomerSelectorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerSelectorViewModelTests.swift; sourceTree = ""; }; B95B15C82B15EBA000A54044 /* UpdateProductInventoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProductInventoryViewModel.swift; sourceTree = ""; }; B96B536A2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPluginsDataProviderTests.swift; sourceTree = ""; }; - B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesForWPComPlansManager.swift; sourceTree = ""; }; B976D5BA2D38089C00D01E2E /* WooShippingCustomsFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingCustomsFormViewModelTests.swift; sourceTree = ""; }; B97C6E552B15E51A008A2BF2 /* UpdateProductInventoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProductInventoryView.swift; sourceTree = ""; }; B98968562A98F227007A2FBE /* TaxEducationalDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxEducationalDialogViewModelTests.swift; sourceTree = ""; }; @@ -5467,7 +5427,6 @@ DEB387A22C36474F0025256E /* GoogleAdsDashboardCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAdsDashboardCard.swift; sourceTree = ""; }; DEB387A42C36479B0025256E /* GoogleAdsDashboardCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAdsDashboardCardViewModel.swift; sourceTree = ""; }; DEB387A62C3682C40025256E /* GoogleAdsCampaign+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleAdsCampaign+UI.swift"; sourceTree = ""; }; - DEBAB70A2A7A3FE000743185 /* StorePlanBannerPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePlanBannerPresenterTests.swift; sourceTree = ""; }; DEBAB70C2A7A6F1100743185 /* MockStorePlanSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStorePlanSynchronizer.swift; sourceTree = ""; }; DEBAB70E2A7A6F3800743185 /* MockConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConnectivityObserver.swift; sourceTree = ""; }; DEC0293629C418FF00FD0E2F /* ApplicationPasswordAuthorizationWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordAuthorizationWebViewController.swift; sourceTree = ""; }; @@ -5584,7 +5543,6 @@ E16715CC2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessWithoutEmailTests.swift; sourceTree = ""; }; E17E3BF8266917C10009D977 /* CardPresentModalScanningFailedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningFailedTests.swift; sourceTree = ""; }; E17E3BFA266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalBluetoothRequiredTests.swift; sourceTree = ""; }; - E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseStoreTests.swift; sourceTree = ""; }; E1ABAEF628479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsSelectPluginView.swift; sourceTree = ""; }; E1B0839A291BC5DD001D99C8 /* WooCommerceTest.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = WooCommerceTest.storekit; sourceTree = ""; }; E1BAAE9F26BBECEF00F2C037 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; @@ -7090,7 +7048,6 @@ DEFD6E5F264990DD00E51E0D /* Plugins */, 576D9F2724DB81BF007B48F4 /* Stats V4 */, CCCFFC5B2934F089006130AF /* Factories */, - 261F1A7A29C2B081001D9861 /* Free Trial */, 02E4FD802306AA890049610C /* StatsTimeRangeBarViewModelTests.swift */, 028E1F712833E954001F8829 /* DashboardViewModelTests.swift */, 027F83EE29B048E2002688C6 /* TopPerformersPeriodViewModelTests.swift */, @@ -7467,22 +7424,11 @@ 261E91A129C9880C00A5C118 /* Upgrades */ = { isa = PBXGroup; children = ( - 261E91A229C9882600A5C118 /* SubscriptionsViewModelTests.swift */, 267C01D029E9B18E00FCC97B /* StorePlanSynchronizerTests.swift */, - 683AA9D52A303CB70099F7BA /* UpgradesViewModelTests.swift */, ); path = Upgrades; sourceTree = ""; }; - 261F1A7A29C2B081001D9861 /* Free Trial */ = { - isa = PBXGroup; - children = ( - 261F1A7B29C2B09D001D9861 /* FreeTrialBannerViewModelTests.swift */, - DEBAB70A2A7A3FE000743185 /* StorePlanBannerPresenterTests.swift */, - ); - path = "Free Trial"; - sourceTree = ""; - }; 262A097F2628A8BF0033AD20 /* View Modifiers */ = { isa = PBXGroup; children = ( @@ -7556,20 +7502,7 @@ 264957A129C5206B0095AA4C /* Upgrades */ = { isa = PBXGroup; children = ( - 264957A229C520860095AA4C /* SubscriptionsView.swift */, - 261E919F29C961EE00A5C118 /* SubscriptionsViewModel.swift */, 267C01CE29E89E1700FCC97B /* StorePlanSynchronizer.swift */, - 68709D3C2A2ED94900A7FA6C /* UpgradesView.swift */, - 68E674A22A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift */, - 68E674A42A4DA8510034BA1E /* PurchaseUpgradeErrorView.swift */, - 68E674A62A4DAAA60034BA1E /* UpgradeWaitingView.swift */, - 68E674A82A4DAB1A0034BA1E /* OwnerUpgradesView.swift */, - 68E674AA2A4DAB8C0034BA1E /* CompletedUpgradeView.swift */, - 68E674AC2A4DAC010034BA1E /* CurrentPlanDetailsView.swift */, - 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */, - 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */, - 6888A2C72A668D650026F5C0 /* FullFeatureListView.swift */, - 6888A2C92A66C42C0026F5C0 /* FullFeatureListViewModel.swift */, ); name = Upgrades; path = Classes/ViewRelated/Upgrades; @@ -7827,7 +7760,6 @@ isa = PBXGroup; children = ( 26C98F9A29C18ACE00F96503 /* StorePlanBanner.swift */, - 2631D4FD29F2141D00F13F20 /* StorePlanBannerPresenter.swift */, 261F1A7829C2AB2E001D9861 /* FreeTrialBannerViewModel.swift */, ); path = "Free Trial"; @@ -8956,6 +8888,17 @@ path = "AI Settings"; sourceTree = ""; }; + 68C1813A2EA23D62002AE846 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 68E674AE2A4DACD50034BA1E /* UpgradeTopBarView.swift */, + 68E674AA2A4DAB8C0034BA1E /* CompletedUpgradeView.swift */, + 68E674A22A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift */, + 6881CCC32A5EE6BF00AEDE36 /* WooPlanCardView.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; 68DF5A8B2CB38EC5000154C9 /* Coupons */ = { isa = PBXGroup; children = ( @@ -9066,7 +9009,6 @@ 0388E1A929E04715007DF84D /* MockDeepLinkNavigator.swift */, B9DC770629F2D4910013B191 /* MockProductSelectorTopProductsProvider.swift */, 026A23FE2A3173F100EFE4BD /* MockBlazeEligibilityChecker.swift */, - 02863F6E2925FC29006A06AA /* MockInAppPurchasesForWPComPlansManager.swift */, 20A3AFE02B0F750B0033AF2D /* MockInPersonPaymentsCashOnDeliveryToggleRowViewModel.swift */, 02FADAA42A607CEE00FE8683 /* MockImageTextScanner.swift */, DEBAB70E2A7A6F3800743185 /* MockConnectivityObserver.swift */, @@ -9684,6 +9626,7 @@ 8CD41D4921F8A7E300CF3C2B /* RELEASE-NOTES.txt */, B559EBAD20A0BF8E00836CD4 /* README.md */, B559EBAE20A0BF8F00836CD4 /* LICENSE */, + 68C1813A2EA23D62002AE846 /* Recovered References */, ); sourceTree = ""; }; @@ -9747,7 +9690,6 @@ 02B1AA6329A4704C00D54FCB /* FilterTabBar */, DEF8CF0829A71ED400800A60 /* JetpackSetup */, 02E3B62629026C83007E0F13 /* Authentication */, - E1325EF928FD543B00EC9B2A /* InAppPurchases */, 03FBDA9B263AD47200ACE257 /* Coupons */, 571B850024CF7E1600CF58A7 /* InAppFeedback */, 0235594024496414004BE2B8 /* BottomSheet */, @@ -10087,7 +10029,6 @@ isa = PBXGroup; children = ( B5DBF3C220E1484400B53AED /* StoresManagerTests.swift */, - E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */, EE57C120297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift */, 020EF5EE2A8C94E0009D2169 /* SiteSnapshotTrackerTests.swift */, ); @@ -12758,14 +12699,6 @@ path = Settings; sourceTree = ""; }; - E1325EF928FD543B00EC9B2A /* InAppPurchases */ = { - isa = PBXGroup; - children = ( - B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */, - ); - path = InAppPurchases; - sourceTree = ""; - }; E138D4F2269ED99A006EA5C6 /* In-Person Payments */ = { isa = PBXGroup; children = ( @@ -14104,7 +14037,6 @@ 0373A12D2A1D1E6000731236 /* BadgedLeftImageTableViewCell.swift in Sources */, 31FE28C225E6D338003519F2 /* LearnMoreTableViewCell.swift in Sources */, 02D45647231CB1FB008CF0A9 /* UIImage+Dot.swift in Sources */, - 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */, E11228BE2707267F004E9F2D /* CardPresentModalUpdateFailedNonRetryable.swift in Sources */, 86DE68822B4BA47A00B437A6 /* BlazeAdDestinationSettingViewModel.swift in Sources */, EEC5E01129A70CC300416CAC /* StoreSetupProgressView.swift in Sources */, @@ -14123,7 +14055,6 @@ EEBDF7DA2A2EF69B00EFEF47 /* ShareProductCoordinator.swift in Sources */, DEFB3011289904E300A620B3 /* WooSetupWebViewModel.swift in Sources */, CE55F2DA2B28796E005D53D7 /* ProductDiscountViewModel.swift in Sources */, - 6888A2CA2A66C42C0026F5C0 /* FullFeatureListViewModel.swift in Sources */, DE02ABBC2B56984A008E0AC4 /* BlazeConfirmPaymentViewModel.swift in Sources */, 02E8B17A23E2C4BD00A43403 /* CircleSpinnerView.swift in Sources */, CCE4CD282669324300E09FD4 /* ShippingLabelPaymentMethodsTopBanner.swift in Sources */, @@ -14583,7 +14514,6 @@ D843D5D322485009001BFA55 /* ShipmentProvidersViewController.swift in Sources */, 02482A8E237BEAE9007E73ED /* AztecLinkFormatBarCommand.swift in Sources */, 02162729237965E8000208D2 /* ProductFormTableViewModel.swift in Sources */, - 68E6749F2A4DA01C0034BA1E /* WooWPComPlan.swift in Sources */, DEF3300C270444070073AE29 /* ShippingLabelSelectedRate.swift in Sources */, CE2A9FBF23BFB1BE002BEC1C /* LedgerTableViewCell.swift in Sources */, 035DBA47292D0995003E5125 /* CardPresentPaymentPreflightController.swift in Sources */, @@ -14639,7 +14569,6 @@ EE4C457F2C368BE3001A3D94 /* ProductDetailPreviewView.swift in Sources */, 0191301F2CF4E80E008C0C88 /* TapToPayEducationStepView.swift in Sources */, B541B226218A412C008FE7C1 /* UIFont+Woo.swift in Sources */, - 68709D402A2EE2DC00A7FA6C /* UpgradesViewModel.swift in Sources */, 4580BA7723F19D4A00B5F764 /* ProductSettingsViewModel.swift in Sources */, DE4B3B5626A68DD000EEF2D8 /* View+InsetPaddings.swift in Sources */, B59D1EEC2190B08B009D1978 /* Age.swift in Sources */, @@ -14731,7 +14660,6 @@ 02C27BCE282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift in Sources */, 2647F7BA292BE2F900D59FDF /* AnalyticsReportCard.swift in Sources */, 451526392577D89E0076B03C /* AddAttributeViewModel.swift in Sources */, - 68709D3D2A2ED94900A7FA6C /* UpgradesView.swift in Sources */, DE0A2EAF281BA278007A8015 /* ProductCategorySelectorViewModel.swift in Sources */, 45B9C64323A91CB6007FC4C5 /* PriceInputFormatter.swift in Sources */, E1F52DC62668E03B00349D75 /* CardPresentModalBluetoothRequired.swift in Sources */, @@ -14739,7 +14667,6 @@ DE1B030D268DD01A00804330 /* ReviewOrderViewController.swift in Sources */, EE45E2BF2A409E250085F227 /* UIColor+Tooltip.swift in Sources */, B5FD111621D3F13700560344 /* BordersView.swift in Sources */, - 68E674AF2A4DACD50034BA1E /* UpgradeTopBarView.swift in Sources */, 0262DA5B23A244830029AF30 /* Product+ShippingSettingsViewModels.swift in Sources */, 02D29A8E29F7C26000473D6D /* InputAccessoryView.swift in Sources */, 20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */, @@ -14783,7 +14710,6 @@ DE87F4082D2D375E00869522 /* FilterHistoryView.swift in Sources */, CEA455C52BB44F9E00D932CF /* ProductsReportItem+Woo.swift in Sources */, DEF36DE92898D3CF00178AC2 /* AuthenticatedWebViewController.swift in Sources */, - 68E674A72A4DAAA60034BA1E /* UpgradeWaitingView.swift in Sources */, D8610D762570AE1F00A5DF27 /* NotWPErrorViewModel.swift in Sources */, 0245465B24EE7637004F531C /* ProductFormEventLoggerProtocol.swift in Sources */, 02C1853D27FF153A00ABD764 /* CardPresentRefundOrchestrator.swift in Sources */, @@ -14996,7 +14922,6 @@ 579CDEFF274D7E7900E8903D /* StoreStatsUsageTracksEventEmitter.swift in Sources */, CEDBDA472B6BEF2E002047D4 /* AnalyticsWebReport.swift in Sources */, 314DC4BD268D158F00444C9E /* CardReaderSettingsKnownReadersProvider.swift in Sources */, - 264957A329C520860095AA4C /* SubscriptionsView.swift in Sources */, 576EA39225264C7400AFC0B3 /* RefundConfirmationViewController.swift in Sources */, DE36E0982A8634FF00B98496 /* StoreNameSetupView.swift in Sources */, EEBB9B3B2D8E5071008D6CE5 /* SelectableShipmentItemRow.swift in Sources */, @@ -15162,14 +15087,12 @@ DE621F6A29D67E1B000DE3BD /* WooAnalyticsEvent+JetpackSetup.swift in Sources */, DE78DE422B2813E4002E58DE /* ThemesCarouselViewModel.swift in Sources */, DEE183F1292E0ED0008818AB /* JetpackSetupInterruptedView.swift in Sources */, - 68E674A92A4DAB1A0034BA1E /* OwnerUpgradesView.swift in Sources */, DE4D23A429B0A9FA003A4B5D /* WPComPasswordLoginView.swift in Sources */, 45A24E5F2451DF1A0050606B /* ProductMenuOrderViewController.swift in Sources */, EE1905882B57BBEC00617C53 /* BlazePaymentMethodsViewModel.swift in Sources */, DEA6BCB12BC6AA040017D671 /* StoreStatsChartViewModel.swift in Sources */, 2667BFE1252FA117008099D4 /* RefundItemQuantityListSelectorCommand.swift in Sources */, 261AA30C2753119E009530FE /* PaymentMethodsViewModel.swift in Sources */, - 68E674AD2A4DAC010034BA1E /* CurrentPlanDetailsView.swift in Sources */, 20134CE82D4D38E000076A80 /* CardPresentPaymentPlugin+SetUpTapToPay.swift in Sources */, DE68B81F26F86B1700C86CFB /* OfflineBannerView.swift in Sources */, B90D21802D1ED1F300ED60ED /* WooShippingCustomsItemOriginCountryInfoDialog.swift in Sources */, @@ -15240,7 +15163,6 @@ 26A630ED253F3B5C00CBC3B1 /* RefundCreationUseCase.swift in Sources */, B95700AE2A72C39C001BADF2 /* CustomerSelectorView.swift in Sources */, DE971219290A9615000C0BD3 /* AddStoreFooterView.swift in Sources */, - 261E91A029C961EE00A5C118 /* SubscriptionsViewModel.swift in Sources */, 4574745D24EA84D800CF49BC /* ProductTypeBottomSheetListSelectorCommand.swift in Sources */, 26578C4126277AFF00A15097 /* OrderAddOnsListViewController.swift in Sources */, 02F67FEE25805F9C00C3BAD2 /* ShippingLabelTrackingURLGenerator.swift in Sources */, @@ -15267,7 +15189,6 @@ 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 */, 028F3F962B0F1A2A00F8E227 /* ConfigurationIndicator.swift in Sources */, B9F3DAAF29BB73CD00DDD545 /* CreateOrderAppIntent.swift in Sources */, @@ -15446,7 +15367,6 @@ 02B334A12BEB712600A46774 /* CollapsibleCustomerCardAddressViewModel.swift in Sources */, 26C98F9B29C18ACE00F96503 /* StorePlanBanner.swift in Sources */, E10BD16D27CF890800CE6449 /* InPersonPaymentsCountryNotSupportedStripe.swift in Sources */, - 68E674AB2A4DAB8C0034BA1E /* CompletedUpgradeView.swift in Sources */, 26F94E26267A559300DB6CCF /* ProductAddOn.swift in Sources */, 4541D88A270718F6005A9E30 /* ShippingLabelCarriersSectionViewModel.swift in Sources */, CE21FB262C2DB3A900303832 /* GoogleAdsCampaignReportCardViewModel.swift in Sources */, @@ -15473,7 +15393,6 @@ 025C006B2550DE4700FAC222 /* CodeScannerViewController.swift in Sources */, 09885C8727C6947A00910A62 /* ProductPriceSettingsValidator.swift in Sources */, A6557218258B7510008AE7CA /* OrderListCellViewModel.swift in Sources */, - 68E674A32A4DA7990034BA1E /* PrePurchaseUpgradesErrorView.swift in Sources */, B9C4AB2527FDE4B6007008B8 /* CardPresentPluginsDataProvider.swift in Sources */, D8C2A28B231931D100F503E9 /* ReviewViewModel.swift in Sources */, B541B223218A29A6008FE7C1 /* NSParagraphStyle+Woo.swift in Sources */, @@ -15643,7 +15562,6 @@ DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */, 451C77712404518600413F73 /* ProductSettingsRows.swift in Sources */, CC200BB127847DE300EC5884 /* OrderPaymentSection.swift in Sources */, - 68E674A12A4DA0B30034BA1E /* InAppPurchasesError.swift in Sources */, 743E272021AEF20100D6DC82 /* FancyAlertViewController+Upgrade.swift in Sources */, B95864082A657D2F002C4C6E /* EnhancedCouponListViewController.swift in Sources */, CEA16F3A20FD0C8C0061B4E1 /* WooAnalytics.swift in Sources */, @@ -15674,7 +15592,6 @@ D817586422BDD81600289CFE /* OrderDetailsDataSource.swift in Sources */, DE58C8A62D8928CF005914DF /* NotificationSettingsViewModel.swift in Sources */, 7E7C5F872719A93C00315B61 /* ProductCategoryListViewController.swift in Sources */, - 2631D4FE29F2141D00F13F20 /* StorePlanBannerPresenter.swift in Sources */, 95968A512CD109D6001F0DA1 /* CustomFieldsListTopBanner.swift in Sources */, 68DF5A8D2CB38EEA000154C9 /* EditableOrderCouponLineViewModel.swift in Sources */, CEC3CC762C934F5500B93FBE /* WooShippingItemsDataSource.swift in Sources */, @@ -15683,7 +15600,6 @@ D8B4D5EE26C2C26C00F34E94 /* InPersonPaymentsStripeAcountReviewView.swift in Sources */, 6850C5EE2B69E6580026A93B /* ReceiptViewModel.swift in Sources */, CE1F512920697F0100C6C810 /* UIFont+Helpers.swift in Sources */, - 68E674A52A4DA8510034BA1E /* PurchaseUpgradeErrorView.swift in Sources */, 2687165A24D350C20042F6AE /* SurveyCoordinatingController.swift in Sources */, 02C88775245036D400E4470F /* FilterProductListViewModel.swift in Sources */, DE6F997E2BEE00C50007B2DD /* InboxDashboardCard.swift in Sources */, @@ -15725,8 +15641,6 @@ DE57462F2B43EB0B0034B10D /* BlazeCampaignCreationForm.swift in Sources */, CCFC50592743E021001E505F /* EditableOrderViewModel.swift in Sources */, CCFBBCF429C4B8AF0081B595 /* ComponentsList.swift in Sources */, - 6881CCC42A5EE6BF00AEDE36 /* WooPlanCardView.swift in Sources */, - B96D6C0729081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift in Sources */, 6856DB2E741639716E149967 /* KeyboardStateProvider.swift in Sources */, B95700AC2A72C1E4001BADF2 /* CustomerSelectorViewController.swift in Sources */, ABC35F18E744C5576B986CB3 /* InPersonPaymentsUnavailableView.swift in Sources */, @@ -15835,7 +15749,6 @@ B555531321B57E8800449E71 /* MockUserNotificationsCenterAdapter.swift in Sources */, 682210ED2909666600814E14 /* CustomerSearchUICommandTests.swift in Sources */, 4590B652261C8D1E00A6FCE0 /* WeightFormatterTests.swift in Sources */, - 683AA9D62A303CB70099F7BA /* UpgradesViewModelTests.swift in Sources */, DE78DE442B2846AF002E58DE /* ThemesCarouselViewModelTests.swift in Sources */, DE4D23B029B1D02A003A4B5D /* WPCom2FALoginViewModelTests.swift in Sources */, 03B9E52F2A150EED005C77F5 /* MockCardReaderSupportDeterminer.swift in Sources */, @@ -15903,7 +15816,6 @@ 0999877427D2819F00F82C65 /* BulkUpdateViewControllerTests.swift in Sources */, B6D2468C2A0ED4C400B79B9C /* EUCustomsScenarioValidatorTests.swift in Sources */, 027EB57829C18AAC003CE551 /* StoreOnboardingLaunchStoreViewModelTests.swift in Sources */, - DEBAB70B2A7A3FE000743185 /* StorePlanBannerPresenterTests.swift in Sources */, CE6302462BAB528900E3325C /* CustomerListViewModelTests.swift in Sources */, 456417F6247D5643001203F6 /* UITableView+HelpersTests.swift in Sources */, B976D5BB2D3808A000D01E2E /* WooShippingCustomsFormViewModelTests.swift in Sources */, @@ -15989,7 +15901,6 @@ 2611EE59243A473300A74490 /* ProductCategoryListViewModelTests.swift in Sources */, 5750BEE82764006F00388BE6 /* RefundFeesDetailsViewModelTests.swift in Sources */, 02691782232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift in Sources */, - 0331A7002A334982001D2C2C /* MockInAppPurchasesForWPComPlansManager.swift in Sources */, 02DF174B2A4A134B008FD33B /* ProductFormActionsFactory+ProductCreationTests.swift in Sources */, 02564A88246C047C00D6DB2A /* Optional+StringTests.swift in Sources */, 2DA63E042E69B6D400B0CB28 /* ApplicationPasswordsExperimentAvailabilityCheckerTests.swift in Sources */, @@ -16027,7 +15938,6 @@ D85136CD231E15B800DD0539 /* MockReviews.swift in Sources */, EE5B5BBD2AB41ED9009BCBD6 /* MockProductCreationAIEligibilityChecker.swift in Sources */, EECB6D212AFC07D500040BC9 /* WooSubscriptionProductsEligibilityCheckerTests.swift in Sources */, - E181CDCC291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift in Sources */, 2655905B27863D1300BB8457 /* MockCollectOrderPaymentUseCase.swift in Sources */, 027F83EF29B048E2002688C6 /* TopPerformersPeriodViewModelTests.swift in Sources */, D8053BCE231F98DA00CE60C2 /* ReviewAgeTests.swift in Sources */, @@ -16418,7 +16328,6 @@ B9DA153E28101BE100FC67DD /* MockOrderRefundsOptionsDeterminer.swift in Sources */, 20D3D4372C65EF72004CE6E3 /* OrdersRouteTests.swift in Sources */, 09C6A26227C01166001FAD73 /* BulkUpdateViewModelTests.swift in Sources */, - 261F1A7C29C2B09D001D9861 /* FreeTrialBannerViewModelTests.swift in Sources */, 6856D806DE7DB61522D54044 /* NSMutableAttributedStringHelperTests.swift in Sources */, 023D69442588C6BD00F7DA72 /* ShippingLabelPaperSizeListSelectorCommandTests.swift in Sources */, 6856DF20E1BDCC391635F707 /* AgeTests.swift in Sources */, @@ -16426,7 +16335,6 @@ 025A1248247CE793008EA761 /* ProductFormViewModel+ObservablesTests.swift in Sources */, BAFEF51E273C2151005F94CC /* SettingsViewModelTests.swift in Sources */, 68A38DF52B293B030090C263 /* MockProductListViewModel.swift in Sources */, - 261E91A329C9882600A5C118 /* SubscriptionsViewModelTests.swift in Sources */, 6856DE479EC3B2265AC1F775 /* Calendar+Extensions.swift in Sources */, 6850C5F42B6A11CA0026A93B /* ReceiptViewModelTests.swift in Sources */, 4596854B254071C000D17B90 /* DownloadableFileBottomSheetListSelectorCommandTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/AppCoordinatorTests.swift b/WooCommerce/WooCommerceTests/AppCoordinatorTests.swift index a4893435a01..650493e2dca 100644 --- a/WooCommerce/WooCommerceTests/AppCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/AppCoordinatorTests.swift @@ -495,7 +495,6 @@ private extension AppCoordinatorTests { loggedOutAppSettings: LoggedOutAppSettingsProtocol = MockLoggedOutAppSettings(), pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager, featureFlagService: FeatureFlagService = MockFeatureFlagService(), - upgradesViewPresentationCoordinator: UpgradesViewPresentationCoordinator = UpgradesViewPresentationCoordinator(), switchStoreUseCase: SwitchStoreUseCaseProtocol? = nil, themeInstaller: ThemeInstaller = DefaultThemeInstaller() ) -> AppCoordinator { @@ -508,7 +507,6 @@ private extension AppCoordinatorTests { loggedOutAppSettings: loggedOutAppSettings, pushNotesManager: pushNotesManager, featureFlagService: featureFlagService, - upgradesViewPresentationCoordinator: upgradesViewPresentationCoordinator, switchStoreUseCase: switchStoreUseCase, themeInstaller: themeInstaller) } diff --git a/WooCommerce/WooCommerceTests/Mocks/MockInAppPurchasesForWPComPlansManager.swift b/WooCommerce/WooCommerceTests/Mocks/MockInAppPurchasesForWPComPlansManager.swift deleted file mode 100644 index fabe76f2ab8..00000000000 --- a/WooCommerce/WooCommerceTests/Mocks/MockInAppPurchasesForWPComPlansManager.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import StoreKit -@testable import WooCommerce - -/// Only used during store creation development before IAP server side is ready. -struct MockInAppPurchasesForWPComPlansManager { - struct Plan: WPComPlanProduct { - let displayName: String - let description: String - let id: String - let displayPrice: String - } - - private let fetchPlansDelayInNanoseconds: UInt64 - private let plans: [WPComPlanProduct] - private let userIsEntitledToPlan: Bool - private let isIAPSupported: Bool - - /// - Parameter fetchPlansDelayInNanoseconds: How long to wait until the mock plan is returned, in nanoseconds. - /// - Parameter plans: WPCom plans to return for purchase. - /// - Parameter userIsEntitledToProduct: Whether the user is entitled to the matched IAP product. - init(fetchPlansDelayInNanoseconds: UInt64 = 1_000_000_000, - plans: [WPComPlanProduct] = Defaults.debugEcommercePlans, - userIsEntitledToPlan: Bool = false, - isIAPSupported: Bool = true) { - self.fetchPlansDelayInNanoseconds = fetchPlansDelayInNanoseconds - self.plans = plans - self.userIsEntitledToPlan = userIsEntitledToPlan - self.isIAPSupported = isIAPSupported - } -} - -extension MockInAppPurchasesForWPComPlansManager: InAppPurchasesForWPComPlansProtocol { - func fetchPlans() async throws -> [WPComPlanProduct] { - try await Task.sleep(nanoseconds: fetchPlansDelayInNanoseconds) - return plans - } - - func userIsEntitledToPlan(with id: String) async throws -> Bool { - userIsEntitledToPlan - } - - func purchasePlan(with id: String, for remoteSiteId: Int64) async throws -> InAppPurchaseResult { - // Returns `.pending` in case of success because `StoreKit.Transaction` cannot be easily mocked. - .pending - } - - func retryWPComSyncForPurchasedPlan(with id: String) async throws { - // no-op - } - - func inAppPurchasesAreSupported() async -> Bool { - isIAPSupported - } - - func siteHasCurrentInAppPurchases(siteID: Int64) async -> Bool { - userIsEntitledToPlan - } -} - -extension MockInAppPurchasesForWPComPlansManager { - enum Defaults { - static let debugEcommercePlans: [WPComPlanProduct] = [ - Plan(displayName: "Debug Monthly", - description: "1 Month of Debug Woo", - id: "debug.woocommerce.ecommerce.monthly", - displayPrice: "$69.99") - ] - static let essentialInAppPurchasesPlans: [WPComPlanProduct] = [ - Plan(displayName: "Essential Monthly", - description: "1 Month of Essential", - id: "woocommerce.express.essential.monthly", - displayPrice: "$99.99") - ] - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/FreeTrialBannerViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/FreeTrialBannerViewModelTests.swift deleted file mode 100644 index f73c7ba2917..00000000000 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/FreeTrialBannerViewModelTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -import XCTest -@testable import WooCommerce -@testable import Networking - -final class FreeTrialBannerViewModelTests: XCTestCase { - - /// All Expiry dates came in GMT from the API. - /// - private static let gmtTimezone = TimeZone(secondsFromGMT: 0) ?? .current - private static let calendar: Calendar = { - var calendar = Calendar(identifier: .gregorian) - calendar.timeZone = gmtTimezone - return calendar - }() - - func test_few_days_left_in_trial() { - // Given - let expiryDate = Date().adding(days: 3, using: Self.calendar)?.startOfDay(timezone: Self.gmtTimezone) - let sitePlan = WPComSitePlan(hasDomainCredit: false, expiryDate: expiryDate) - - // When - let viewModel = FreeTrialBannerViewModel(sitePlan: sitePlan, timeZone: Self.gmtTimezone, calendar: Self.calendar) - - // Then - XCTAssertEqual(viewModel.message, NSLocalizedString("3 days left in your trial.", comment: "")) - } - - func test_1_day_left_in_trial() { - // Given - let expiryDate = Date().adding(days: 1, using: Self.calendar)?.startOfDay(timezone: Self.gmtTimezone) - let sitePlan = WPComSitePlan(hasDomainCredit: false, expiryDate: expiryDate) - - // When - let viewModel = FreeTrialBannerViewModel(sitePlan: sitePlan, timeZone: Self.gmtTimezone, calendar: Self.calendar) - - // Then - XCTAssertEqual(viewModel.message, NSLocalizedString("1 day left in your trial.", comment: "")) - } - - func test_0_days_left_on_trial() { - // Given - let expiryDate = Date().startOfDay(timezone: Self.gmtTimezone) - let sitePlan = WPComSitePlan(hasDomainCredit: false, expiryDate: expiryDate) - - // When - let viewModel = FreeTrialBannerViewModel(sitePlan: sitePlan, timeZone: Self.gmtTimezone, calendar: Self.calendar) - - // Then - XCTAssertEqual(viewModel.message, NSLocalizedString("Your trial has ended.", comment: "")) - } - - func test_no_days_left_on_trial() { - // Given - let expiryDate = Date().adding(days: -3, using: Self.calendar)?.startOfDay(timezone: Self.gmtTimezone) - let sitePlan = WPComSitePlan(hasDomainCredit: false, expiryDate: expiryDate) - - // When - let viewModel = FreeTrialBannerViewModel(sitePlan: sitePlan, timeZone: Self.gmtTimezone, calendar: Self.calendar) - - // Then - XCTAssertEqual(viewModel.message, NSLocalizedString("Your trial has ended.", comment: "")) - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenterTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenterTests.swift deleted file mode 100644 index 5a6087a13fd..00000000000 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Free Trial/StorePlanBannerPresenterTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -import XCTest -import Yosemite -import Combine -@testable import WooCommerce - -final class StorePlanBannerPresenterTests: XCTestCase { - - private let siteID: Int64 = 123 - private var planSynchronizer: MockStorePlanSynchronizer! - private var inAppPurchaseManager: MockInAppPurchasesForWPComPlansManager! - private var containerView: UIView! - // Keep strong reference to the presenter - private var presenter: StorePlanBannerPresenter? - - override func setUp() { - planSynchronizer = MockStorePlanSynchronizer() - inAppPurchaseManager = MockInAppPurchasesForWPComPlansManager(isIAPSupported: true) - containerView = UIView() - super.setUp() - } - - override func tearDown() { - planSynchronizer = nil - inAppPurchaseManager = nil - containerView = nil - super.tearDown() - } - - func test_banner_is_displayed_when_site_plan_is_free_trial() { - // Given - let defaultSite = Site.fake().copy(siteID: siteID, isWordPressComStore: true) - let stores = MockStoresManager(sessionManager: .makeForTesting(defaultSite: defaultSite)) - presenter = StorePlanBannerPresenter(viewController: UIViewController(), - containerView: containerView, - siteID: siteID, - onLayoutUpdated: { _ in }, - stores: stores, - storePlanSynchronizer: planSynchronizer, - inAppPurchasesManager: inAppPurchaseManager) - - // When - let plan = WPComSitePlan(id: "1052", hasDomainCredit: false, expiryDate: Date()) - planSynchronizer.setState(.loaded(plan)) - - // Then - waitUntil { - self.containerView.subviews.isNotEmpty - } - } - - func test_banner_is_displayed_when_site_plan_is_free_and_site_was_ecommerce_trial() { - // Given - let defaultSite = Site.fake().copy(siteID: siteID, isWordPressComStore: false, wasEcommerceTrial: true) - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, defaultSite: defaultSite)) - presenter = StorePlanBannerPresenter(viewController: UIViewController(), - containerView: containerView, - siteID: siteID, - onLayoutUpdated: { _ in }, - stores: stores, - storePlanSynchronizer: planSynchronizer, - inAppPurchasesManager: inAppPurchaseManager) - - // When - let plan = WPComSitePlan(id: "1", hasDomainCredit: false) - planSynchronizer.setState(.loaded(plan)) - - // Then - waitUntil { - self.containerView.subviews.isNotEmpty - } - } - - func test_banner_is_dismissed_when_connection_is_lost() { - // Given - let defaultSite = Site.fake().copy(siteID: siteID, isWordPressComStore: false, wasEcommerceTrial: true) - let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, defaultSite: defaultSite)) - let connectivityObserver = MockConnectivityObserver() - presenter = StorePlanBannerPresenter(viewController: UIViewController(), - containerView: containerView, - siteID: siteID, - onLayoutUpdated: { _ in }, - stores: stores, - storePlanSynchronizer: planSynchronizer, - connectivityObserver: connectivityObserver, - inAppPurchasesManager: inAppPurchaseManager) - - // When - let plan = WPComSitePlan(id: "1", hasDomainCredit: false) - planSynchronizer.setState(.loaded(plan)) - - // Then - waitUntil { - self.containerView.subviews.isNotEmpty - } - - // When - connectivityObserver.setStatus(.notReachable) - - // Then - waitUntil { - self.containerView.subviews.isEmpty - } - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift index 2e3a34d9faf..70a7ffb38ad 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift @@ -535,61 +535,6 @@ final class HubMenuViewModelTests: XCTestCase { XCTAssertFalse(viewModel.shouldAuthenticateAdminPage) } - @MainActor - func test_menuElements_include_subscriptions_on_wp_com_sites_if_not_free_trial() { - // Given - let sessionManager = SessionManager.testingInstance - sessionManager.defaultSite = Site.fake().copy(isWordPressComStore: true) - let stores = MockStoresManager(sessionManager: sessionManager) - - // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, - tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), - stores: stores) - viewModel.setupMenuElements() - - XCTAssertNotNil(viewModel.settingsElements.firstIndex(where: { item in - item.id == HubMenuViewModel.Subscriptions.id - })) - } - - @MainActor - func test_menuElements_does_not_include_subscriptions_on_wp_com_free_trial_sites() { - // Given - let freeTrialPlanSlug = "ecommerce-trial-bundle-monthly" - let sessionManager = SessionManager.testingInstance - sessionManager.defaultSite = Site.fake().copy(plan: freeTrialPlanSlug, isWordPressComStore: true) - let stores = MockStoresManager(sessionManager: sessionManager) - - // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, - tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), - stores: stores) - viewModel.setupMenuElements() - - XCTAssertNil(viewModel.settingsElements.firstIndex(where: { item in - item.id == HubMenuViewModel.Subscriptions.id - })) - } - - @MainActor - func test_menuElements_does_not_include_subscriptions_on_self_hosted_sites() { - // Given - let sessionManager = SessionManager.testingInstance - sessionManager.defaultSite = Site.fake().copy(isWordPressComStore: false) - let stores = MockStoresManager(sessionManager: sessionManager) - - // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, - tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), - stores: stores) - viewModel.setupMenuElements() - - XCTAssertNil(viewModel.settingsElements.firstIndex(where: { item in - item.id == HubMenuViewModel.Subscriptions.id - })) - } - @MainActor func test_menuElements_include_customers() { // Given @@ -653,7 +598,6 @@ final class HubMenuViewModelTests: XCTestCase { let expectedMenusAndDestinations: [HubMenuNavigationDestination: HubMenuItem] = [ .settings: HubMenuViewModel.Settings(), .payments: HubMenuViewModel.Payments(), - .subscriptions: HubMenuViewModel.Subscriptions(), .blaze: HubMenuViewModel.Blaze(), .wooCommerceAdmin: HubMenuViewModel.WoocommerceAdmin(), .viewStore: HubMenuViewModel.ViewStore(), @@ -799,7 +743,6 @@ final class HubMenuViewModelTests: XCTestCase { let otherMenuItems: [HubMenuItem] = [ HubMenuViewModel.Settings(), HubMenuViewModel.Payments(), - HubMenuViewModel.Subscriptions(), HubMenuViewModel.Blaze(), HubMenuViewModel.WoocommerceAdmin(), HubMenuViewModel.ViewStore(), diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/SubscriptionsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/SubscriptionsViewModelTests.swift deleted file mode 100644 index 49b317bf843..00000000000 --- a/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/SubscriptionsViewModelTests.swift +++ /dev/null @@ -1,189 +0,0 @@ -import XCTest -@testable import WooCommerce -@testable import Yosemite - -final class SubscriptionsViewModelTests: XCTestCase { - - let freeTrialID = "1052" - let sampleSite = Site.fake().copy(siteID: 123, isWordPressComStore: true) - - func test_active_free_trial_plan_has_correct_view_model_values() { - // Given - let expireDate = Date().addingDays(14) - let plan = WPComSitePlan(id: freeTrialID, - hasDomainCredit: false, - expiryDate: expireDate) - - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - let featureFlags = MockFeatureFlagService() - session.defaultSite = sampleSite - - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - completion(.success(plan)) - default: - break - } - } - - // When - let synchronizer = StorePlanSynchronizer(stores: stores) - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer, featureFlagService: featureFlags) - viewModel.loadPlan() - - // Then - XCTAssertEqual(viewModel.planName, NSLocalizedString("Free Trial", comment: "")) - XCTAssertTrue(viewModel.planInfo.isNotEmpty) - XCTAssertFalse(viewModel.shouldShowManageSubscriptionButton) - XCTAssertNil(viewModel.errorNotice) - } - - func test_expired_free_trial_plan_has_correct_view_model_values() { - // Given - let expireDate = Date().addingDays(-3) - let plan = WPComSitePlan(id: freeTrialID, - hasDomainCredit: false, - expiryDate: expireDate) - - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - session.defaultSite = sampleSite - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - completion(.success(plan)) - default: - break - } - } - let synchronizer = StorePlanSynchronizer(stores: stores) - let featureFlags = MockFeatureFlagService() - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer, featureFlagService: featureFlags) - - // When - viewModel.loadPlan() - - // Then - XCTAssertEqual(viewModel.planName, NSLocalizedString("Trial ended", comment: "")) - XCTAssertTrue(viewModel.planInfo.isNotEmpty) - XCTAssertFalse(viewModel.shouldShowManageSubscriptionButton) - XCTAssertNil(viewModel.errorNotice) - } - - func test_active_regular_plan_has_correct_view_model_values() { - // Given - let expireDate = Date().addingDays(300) - let plan = WPComSitePlan(id: "another-id", - hasDomainCredit: false, - expiryDate: expireDate, - name: "WordPress.com eCommerce") - - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - session.defaultSite = sampleSite - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - completion(.success(plan)) - default: - break - } - } - let synchronizer = StorePlanSynchronizer(stores: stores) - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer) - - // When - viewModel.loadPlan() - - // Then - XCTAssertEqual(viewModel.planName, NSLocalizedString("eCommerce", comment: "")) - XCTAssertTrue(viewModel.planInfo.isNotEmpty) - XCTAssertFalse(viewModel.shouldShowManageSubscriptionButton) - XCTAssertNil(viewModel.errorNotice) - } - - func test_WooExpress_is_removed_from_plan_name() { - // Given - let expireDate = Date().addingDays(300) - let plan = WPComSitePlan(id: "another-id", - hasDomainCredit: false, - expiryDate: expireDate, - name: "Woo Express: Essential") - - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - session.defaultSite = sampleSite - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - completion(.success(plan)) - default: - break - } - } - let synchronizer = StorePlanSynchronizer(stores: stores) - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer) - - // When - viewModel.loadPlan() - - // Then - XCTAssertEqual(viewModel.planName, NSLocalizedString("Essential", comment: "")) - } - - func test_error_fetching_plan_has_correct_view_model_values() { - // Given - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - session.defaultSite = sampleSite - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - let error = NSError(domain: "", code: 0) - completion(.failure(error)) - default: - break - } - } - let synchronizer = StorePlanSynchronizer(stores: stores) - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer) - - // When - viewModel.loadPlan() - - // Then - XCTAssertTrue(viewModel.planName.isEmpty) - XCTAssertTrue(viewModel.planInfo.isEmpty) - XCTAssertFalse(viewModel.shouldShowManageSubscriptionButton) - XCTAssertNotNil(viewModel.errorNotice) - } - - func test_expired_unknown_plan_has_correct_view_model_values() { - // Given - let session = SessionManager.testingInstance - let stores = MockStoresManager(sessionManager: session) - session.defaultSite = sampleSite - stores.whenReceivingAction(ofType: PaymentAction.self) { action in - switch action { - case .loadSiteCurrentPlan(_, let completion): - completion(.failure(LoadSiteCurrentPlanError.noCurrentPlan)) - default: - break - } - } - let synchronizer = StorePlanSynchronizer(stores: stores) - let featureFlags = MockFeatureFlagService() - let viewModel = SubscriptionsViewModel(stores: stores, storePlanSynchronizer: synchronizer, featureFlagService: featureFlags) - - // When - viewModel.loadPlan() - - // Then - assertEqual("plan ended", viewModel.planName) - assertEqual("Your subscription has ended and you have limited access to all the features.", viewModel.planInfo) - XCTAssertFalse(viewModel.shouldShowManageSubscriptionButton) - XCTAssertNil(viewModel.errorNotice) - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/UpgradesViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/UpgradesViewModelTests.swift deleted file mode 100644 index a666c03513c..00000000000 --- a/WooCommerce/WooCommerceTests/ViewRelated/Upgrades/UpgradesViewModelTests.swift +++ /dev/null @@ -1,131 +0,0 @@ -import XCTest -@testable import WooCommerce -import Yosemite - -final class UpgradesViewModelTests: XCTestCase { - - private let sampleSiteID: Int64 = 12345 - private var mockInAppPurchasesManager: MockInAppPurchasesForWPComPlansManager! - private var stores: MockStoresManager! - - private var sut: UpgradesViewModel! - - @MainActor - func createSut(alreadySubscribed: Bool = false, - isSiteOwner: Bool = true, - isIAPSupported: Bool = true, - plans: [WPComPlanProduct] = MockInAppPurchasesForWPComPlansManager.Defaults.essentialInAppPurchasesPlans) { - - mockInAppPurchasesManager = MockInAppPurchasesForWPComPlansManager(plans: plans, - userIsEntitledToPlan: alreadySubscribed, - isIAPSupported: isIAPSupported) - - let site = Site.fake().copy(isSiteOwner: isSiteOwner) - - stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, defaultSite: site)) - stores.whenReceivingAction(ofType: FeatureFlagAction.self) { action in - switch action { - case .isRemoteFeatureFlagEnabled(.hardcodedPlanUpgradeDetailsMilestone1AreAccurate, defaultValue: _, let completion): - completion(true) - default: - break - } - } - - sut = UpgradesViewModel(siteID: sampleSiteID, inAppPurchasesPlanManager: mockInAppPurchasesManager, stores: stores) - } - - func test_if_user_has_active_in_app_purchases_then_returns_maximum_sites_upgraded_error() async { - // Given - await createSut(alreadySubscribed: true) - - // When - await sut.prepareViewModel() - - // Then - assertEqual(.prePurchaseError(.maximumSitesUpgraded), sut.upgradeViewState) - } - - func test_upgrades_when_fetchPlans_is_invoked_then_fetch_mocked_wpcom_plan() async { - // Given - await createSut() - - // When - await sut.prepareViewModel() - - // Then - guard case .loaded(let plans) = sut.upgradeViewState, - let plan = plans.first else { - return XCTFail("expected `.loaded` state not found") - } - assertEqual("Essential Monthly", plan.wpComPlan.displayName) - assertEqual("1 Month of Essential", plan.wpComPlan.description) - assertEqual("woocommerce.express.essential.monthly", plan.wpComPlan.id) - assertEqual("$99.99", plan.wpComPlan.displayPrice) - } - - func test_upgrades_when_retrievePlanDetailsIfAvailable_retrieves_injected_wpcom_plan() async { - // Given - let expectedPlan: WPComPlanProduct = MockInAppPurchasesForWPComPlansManager.Plan( - displayName: "Test awesome plan", - description: "All the Woo, all the time", - id: "woocommerce.express.essential.monthly", - displayPrice: "$1.50") - - await createSut(plans: [expectedPlan]) - - // When - await sut.prepareViewModel() - - // Then - guard case .loaded(let plans) = sut.upgradeViewState, - let plan = plans.first else { - return XCTFail("expected `.loaded` state not found") - } - assertEqual("Test awesome plan", plan.wpComPlan.displayName) - assertEqual("All the Woo, all the time", plan.wpComPlan.description) - assertEqual("woocommerce.express.essential.monthly", plan.wpComPlan.id) - assertEqual("$1.50", plan.wpComPlan.displayPrice) - } - - func test_upgradeViewState_when_initialized_is_loading_state() async { - // Given, When - await createSut() - - // Then - assertEqual(.loading, sut.upgradeViewState) - } - - func test_upgradeViewState_when_prepareViewModel_by_non_owner_then_state_is_prepurchaseError_userNotAllowedToUpgrade() async { - // Given - await createSut(isSiteOwner: false) - - // When - await sut.prepareViewModel() - - // Then - assertEqual(.prePurchaseError(.userNotAllowedToUpgrade), sut.upgradeViewState) - } - - func test_upgradeViewState_when_IAP_are_not_supported_and_prepareViewModel_then_state_is_inAppPurchasesNotSupported() async { - // Given - await createSut(isIAPSupported: false) - - // When - await sut.prepareViewModel() - - // Then - assertEqual(.prePurchaseError(.inAppPurchasesNotSupported), sut.upgradeViewState) - } - - func test_upgradeViewState_when_retrievePlanDetailsIfAvailable_fails_and_prepareViewModel_then_state_is_fetchError() async { - // Given - await createSut(plans: []) - - // When - await sut.prepareViewModel() - - // Then - assertEqual(.prePurchaseError(.fetchError), sut.upgradeViewState) - } -} diff --git a/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift b/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift deleted file mode 100644 index f937a5c47ff..00000000000 --- a/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift +++ /dev/null @@ -1,228 +0,0 @@ -import XCTest -import TestKit -import StoreKitTest - -@testable import Yosemite -@testable import Networking - -final class InAppPurchaseStoreTests: XCTestCase { - - /// Mock Network: Allows us to inject predefined responses! - /// - private var network: MockNetwork! - - /// Mock Storage: InMemory - /// - private var storageManager: MockStorageManager! - - private var storeKitSession = try! SKTestSession(configurationFileNamed: "WooCommerceTest") - - /// Testing SiteID - /// - private let sampleSiteID: Int64 = 123 - - /// Testing Product ID - /// Should match the product ID in WooCommerce.storekit - /// - private let sampleProductID: String = "debug.woocommerce.ecommerce.monthly" - - /// Testing Order ID - /// Should match the order ID in iap-order-create.json - /// - private let sampleOrderID: Int64 = 12345 - - var store: InAppPurchaseStore! - - - override func setUp() { - network = MockNetwork(useResponseQueue: true) - storageManager = MockStorageManager() - store = InAppPurchaseStore(dispatcher: Dispatcher(), storageManager: storageManager, network: network) - storeKitSession.disableDialogs = true - } - - override func tearDown() { - storeKitSession.resetToDefaultState() - storeKitSession.clearTransactions() - } - - func test_iap_supported_in_us() throws { - // Given - storeKitSession.storefront = "USA" - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.inAppPurchasesAreSupported { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - XCTAssertTrue(result) - } - - func test_iap_supported_in_canada() throws { - try skipBecauseOfStoreFrontXcode16Issue() - - // Given - storeKitSession.storefront = "CAN" - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.inAppPurchasesAreSupported { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - XCTAssertFalse(result) - } - - func test_load_products_loads_products_response() throws { - // Given - network.simulateResponse(requestUrlSuffix: "iap/products", filename: "iap-products") - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.loadProducts { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - let products = try XCTUnwrap(result.get()) - XCTAssertFalse(products.isEmpty) - XCTAssertEqual(products.first?.id, sampleProductID) - } - - func test_load_products_fails_if_iap_unsupported() throws { - try skipBecauseOfStoreFrontXcode16Issue() - - // Given - storeKitSession.storefront = "CAN" - network.simulateResponse(requestUrlSuffix: "iap/products", filename: "iap-products") - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.loadProducts { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - XCTAssert(result.isFailure) - } - - // TODO: re-enable the test case when it can pass consistently. - func test_purchase_product_completes_purchase() throws { - // Given - network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "iap-order-create") - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - let purchaseResult = try XCTUnwrap(result.get()) - guard case let .success(verificationResult) = purchaseResult, - case let .verified(transaction) = verificationResult else { - return XCTFail() - } - XCTAssertEqual(transaction.productID, sampleProductID) - XCTAssertNotNil(transaction.appAccountToken) - } - - @available(iOS 16.0, *) - func test_purchase_product_ensure_xcode_environment() throws { - // Given - network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "iap-order-create") - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - let purchaseResult = try XCTUnwrap(result.get()) - guard case let .success(verificationResult) = purchaseResult, - case let .verified(transaction) = verificationResult else { - return XCTFail() - } - XCTAssertEqual(transaction.environment, .xcode) - } - - func test_purchase_product_handles_api_errors() throws { - // Given - network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "error-wp-rest-forbidden") - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - XCTAssert(result.isFailure) - let error = try XCTUnwrap(result.failure) - XCTAssert(error is WordPressApiError) - } - - // TODO: re-enable the test case when it can pass consistently. More details: - // https://github.com/woocommerce/woocommerce-ios/pull/8256#pullrequestreview-1199236279 - func test_user_is_entitled_to_product_returns_false_when_not_entitled() throws { - // Given - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.userIsEntitledToProduct(productID: self.sampleProductID) { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - let isEntitled = try XCTUnwrap(result.get()) - XCTAssertFalse(isEntitled) - } - - // TODO: re-enable the test case when it can pass consistently. More details: - // https://github.com/woocommerce/woocommerce-ios/pull/8256#pullrequestreview-1199236279 - func test_user_is_entitled_to_product_returns_true_when_entitled() throws { - // Given - try storeKitSession.buyProduct(productIdentifier: sampleProductID) - - // When - let result = waitFor { promise in - let action = InAppPurchaseAction.userIsEntitledToProduct(productID: self.sampleProductID) { result in - promise(result) - } - self.store.onAction(action) - } - - // Then - let isEntitled = try XCTUnwrap(result.get()) - XCTAssertTrue(isEntitled) - } -} - -private extension InAppPurchaseStoreTests { - /// Setting storefront value in Xcode 16 in one test no longer resets in other tests, even when resetToDefaultState is called - /// Reason unknown - /// https://forums.developer.apple.com/forums/thread/764937 - func skipBecauseOfStoreFrontXcode16Issue() throws { - try XCTSkipIf(true, "Setting storefront value in Xcode 16 in one test no longer resets in other tests, even when resetToDefaultState is called") - } -}