diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 089b27a6356..c8fbce41199 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -64,6 +64,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .productsBulkEditing: return buildConfig == .localDeveloper || buildConfig == .alpha + case .domainSettings: + return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index d6cec708597..661b17da974 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -149,4 +149,8 @@ public enum FeatureFlag: Int { /// Bulk editing of status and price in products list /// case productsBulkEditing + + /// Whether to enable domain updates from the settings for a WPCOM site. + /// + case domainSettings } diff --git a/Networking/Networking/Remote/DomainRemote.swift b/Networking/Networking/Remote/DomainRemote.swift index df9b074b912..2f2d7b039eb 100644 --- a/Networking/Networking/Remote/DomainRemote.swift +++ b/Networking/Networking/Remote/DomainRemote.swift @@ -41,6 +41,24 @@ public struct FreeDomainSuggestion: Decodable, Equatable { } } +/// Necessary data for a site's domain. +public struct SiteDomain: Equatable { + /// Domain name. + public let name: String + + /// Whether the domain is the site's primary domain. + public let isPrimary: Bool + + /// The next renewal date, if available. + public let renewalDate: Date? + + public init(name: String, isPrimary: Bool, renewalDate: Date? = nil) { + self.name = name + self.isPrimary = isPrimary + self.renewalDate = renewalDate + } +} + // MARK: - Constants // private extension DomainRemote { diff --git a/Networking/Networking/Remote/PaymentRemote.swift b/Networking/Networking/Remote/PaymentRemote.swift index 786e11dedc1..d67f964e115 100644 --- a/Networking/Networking/Remote/PaymentRemote.swift +++ b/Networking/Networking/Remote/PaymentRemote.swift @@ -68,6 +68,19 @@ public struct WPComPlan: Decodable, Equatable { } } +/// Contains necessary data for a site's WPCOM plan. +public struct WPComSitePlan { + /// WPCOM plan of a site. + public let plan: WPComPlan + /// Whether a site has domain credit from the WPCOM plan. + public let hasDomainCredit: Bool + + public init(plan: WPComPlan, hasDomainCredit: Bool) { + self.plan = plan + self.hasDomainCredit = hasDomainCredit + } +} + /// Possible error cases from loading a WPCOM plan. public enum LoadPlanError: Error { case noMatchingPlan diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsView.swift new file mode 100644 index 00000000000..ed43261a4b5 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsView.swift @@ -0,0 +1,157 @@ +import SwiftUI + +/// Hosting controller that wraps the `DomainSettingsView` view. +final class DomainSettingsHostingController: UIHostingController { + init(viewModel: DomainSettingsViewModel) { + super.init(rootView: DomainSettingsView(viewModel: viewModel)) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureTransparentNavigationBar() + } +} + +/// Shows a site's domains with actions to add a domain or redeem a domain credit. +struct DomainSettingsView: View { + @ObservedObject private var viewModel: DomainSettingsViewModel + + init(viewModel: DomainSettingsViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: Layout.contentSpacing) { + if let freeDomain = viewModel.freeStagingDomain { + HStack { + FreeStagingDomainView(domain: freeDomain) + Spacer() + } + } + + if viewModel.hasDomainCredit { + // TODO: 8558 - domain credit UI with redemption action + } + + if viewModel.domains.isNotEmpty { + // TODO: 8558 - show domain list with search domain action + } + } + .padding(Layout.contentPadding) + } + .safeAreaInset(edge: .bottom) { + if viewModel.domains.isEmpty { + VStack { + Divider() + .frame(height: Layout.dividerHeight) + .foregroundColor(Color(.separator)) + Button(Localization.searchDomainButton) { + // TODO: 8558 - search domain action + } + .buttonStyle(PrimaryButtonStyle()) + .padding(Layout.bottomContentPadding) + } + .background(Color(.systemBackground)) + } + } + .navigationBarTitle(Localization.title) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + viewModel.onAppear() + } + } +} + +private extension DomainSettingsView { + enum Localization { + static let title = NSLocalizedString("Domain", comment: "Navigation bar title of the domain settings screen.") + static let searchDomainButton = NSLocalizedString( + "Search for a Domain", + comment: "Title of the button on the domain settings screen to search for a domain." + ) + } +} + +private extension DomainSettingsView { + enum Layout { + static let dividerHeight: CGFloat = 1 + static let bottomContentPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16) + static let contentPadding: EdgeInsets = .init(top: 39, leading: 16, bottom: 16, trailing: 16) + static let contentSpacing: CGFloat = 36 + } +} + +#if DEBUG + +import Yosemite +import enum Networking.DotcomError + +/// StoresManager that specifically handles actions for `DomainSettingsView` previews. +final class DomainSettingsViewStores: DefaultStoresManager { + private let domainsResult: Result<[SiteDomain], Error> + private let sitePlanResult: Result + + init(domainsResult: Result<[SiteDomain], Error>, + sitePlanResult: Result) { + self.domainsResult = domainsResult + self.sitePlanResult = sitePlanResult + super.init(sessionManager: ServiceLocator.stores.sessionManager) + } + + override func dispatch(_ action: Action) { + if let action = action as? DomainAction { + if case let .loadDomains(_, completion) = action { + completion(domainsResult) + } + } else if let action = action as? PaymentAction { + if case let .loadSiteCurrentPlan(_, completion) = action { + completion(sitePlanResult) + } + } + } +} + +struct DomainSettingsView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + DomainSettingsView(viewModel: + .init(siteID: 134, + stores: DomainSettingsViewStores( + // There is one free domain and two paid domains. + domainsResult: .success([ + .init(name: "free.test", isPrimary: true), + .init(name: "one.test", isPrimary: false, renewalDate: .distantFuture), + .init(name: "duo.test", isPrimary: true, renewalDate: .now) + ]), + // The site has domain credit. + sitePlanResult: .success(.init(plan: .init(productID: 0, + name: "", + formattedPrice: ""), + hasDomainCredit: true))))) + } + + NavigationView { + DomainSettingsView(viewModel: + .init(siteID: 134, + stores: DomainSettingsViewStores( + // There is one free domain and no other paid domains. + domainsResult: .success([ + .init(name: "free.test", isPrimary: true) + ]), + sitePlanResult: .success(.init(plan: .init(productID: 0, + name: "", + formattedPrice: ""), + hasDomainCredit: true))))) + } + } + } +} + +#endif diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsViewModel.swift new file mode 100644 index 00000000000..61ff8979f8b --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSettingsViewModel.swift @@ -0,0 +1,70 @@ +import Foundation +import Yosemite + +/// View model for `DomainSettingsView`. +final class DomainSettingsViewModel: ObservableObject { + struct Domain { + /// Whether the domain is the site's primary domain. + let isPrimary: Bool + + /// The address of the domain. + let name: String + + // The next renewal date. + let autoRenewalDate: Date? + } + + struct FreeStagingDomain { + /// Whether the domain is the site's primary domain. + let isPrimary: Bool + + /// The address of the domain. + let name: String + } + + @Published private(set) var hasDomainCredit: Bool = false + @Published private(set) var domains: [Domain] = [] + @Published private(set) var freeStagingDomain: FreeStagingDomain? + + private let siteID: Int64 + private let stores: StoresManager + + init(siteID: Int64, stores: StoresManager = ServiceLocator.stores) { + self.siteID = siteID + self.stores = stores + } + + func onAppear() { + stores.dispatch(DomainAction.loadDomains(siteID: siteID) { [weak self] result in + self?.handleDomainsResult(result) + }) + + stores.dispatch(PaymentAction.loadSiteCurrentPlan(siteID: siteID) { [weak self] result in + self?.handleSiteCurrentPlanResult(result) + }) + } +} + +private extension DomainSettingsViewModel { + func handleDomainsResult(_ result: Result<[SiteDomain], Error>) { + switch result { + case .success(let domains): + let stagingDomain = domains.first(where: { $0.renewalDate == nil }) + freeStagingDomain = stagingDomain + .map { FreeStagingDomain(isPrimary: $0.isPrimary, name: $0.name) } + self.domains = domains.filter { $0 != stagingDomain } + .map { Domain(isPrimary: $0.isPrimary, name: $0.name, autoRenewalDate: $0.renewalDate) } + case .failure(let error): + DDLogError("⛔️ Error retrieving domains for siteID \(siteID): \(error)") + } + } + + func handleSiteCurrentPlanResult(_ result: Result) { + switch result { + case .success(let sitePlan): + hasDomainCredit = sitePlan.hasDomainCredit + case .failure(let error): + DDLogError("⛔️ Error retrieving site plan for siteID \(siteID): \(error)") + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/FreeStagingDomainView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/FreeStagingDomainView.swift new file mode 100644 index 00000000000..133a5611e9a --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/FreeStagingDomainView.swift @@ -0,0 +1,59 @@ +import SwiftUI + +/// Shows a site's free staging domain, with an optional badge if the domain is the primary domain. +struct FreeStagingDomainView: View { + let domain: DomainSettingsViewModel.FreeStagingDomain + + var body: some View { + VStack(alignment: .leading, spacing: Layout.contentSpacing) { + VStack(alignment: .leading, spacing: 0) { + Text(Localization.freeDomainTitle) + Text(domain.name) + .bold() + } + if domain.isPrimary { + // TODO: 8558 - refactor to reuse `BadgeView` + Text(Localization.primaryDomainNotice) + .foregroundColor(Color(.textBrand)) + .padding(.leading, Layout.horizontalPadding) + .padding(.trailing, Layout.horizontalPadding) + .padding(.top, Layout.verticalPadding) + .padding(.bottom, Layout.verticalPadding) + .background(RoundedRectangle(cornerRadius: Layout.cornerRadius) + .fill(Color(.withColorStudio(.wooCommercePurple, shade: .shade0)))) + .font(.system(size: 12, weight: .bold)) + } + } + } +} + +private extension FreeStagingDomainView { + enum Localization { + static let freeDomainTitle = NSLocalizedString( + "Your free store address", + comment: "Title of the free domain view." + ) + static let primaryDomainNotice = NSLocalizedString( + "Primary site address", + comment: "Title for a free domain if the domain is the primary site address." + ) + } +} + +private extension FreeStagingDomainView { + enum Layout { + static let horizontalPadding: CGFloat = 6 + static let verticalPadding: CGFloat = 4 + static let cornerRadius: CGFloat = 8 + static let contentSpacing: CGFloat = 8 + } +} + +struct FreeStagingDomainView_Previews: PreviewProvider { + static var previews: some View { + VStack { + FreeStagingDomainView(domain: .init(isPrimary: true, name: "go.trees")) + FreeStagingDomainView(domain: .init(isPrimary: false, name: "go.trees")) + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift index 3a1f566e3a0..1c606114566 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift @@ -134,6 +134,8 @@ private extension SettingsViewController { configurePlugins(cell: cell) case let cell as HostingTableViewCell where row == .woocommerceDetails: configureWooCommmerceDetails(cell: cell) + case let cell as BasicTableViewCell where row == .domain: + configureDomain(cell: cell) case let cell as BasicTableViewCell where row == .installJetpack: configureInstallJetpack(cell: cell) case let cell as BasicTableViewCell where row == .support: @@ -189,6 +191,12 @@ private extension SettingsViewController { cell.textLabel?.text = Localization.helpAndSupport } + func configureDomain(cell: BasicTableViewCell) { + cell.accessoryType = .disclosureIndicator + cell.selectionStyle = .default + cell.textLabel?.text = Localization.domain + } + func configureInstallJetpack(cell: BasicTableViewCell) { cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default @@ -334,6 +342,18 @@ private extension SettingsViewController { show(viewController, sender: self) } + func domainWasPressed() { + guard let site = ServiceLocator.stores.sessionManager.defaultSite else { + return + } + + // TODO: 8558 - analytics + + let domainSettings = DomainSettingsHostingController(viewModel: .init(siteID: site.siteID)) + let navigationController = WooNavigationController(rootViewController: domainSettings) + present(navigationController, animated: true) + } + func installJetpackWasPressed() { guard let site = ServiceLocator.stores.sessionManager.defaultSite else { return @@ -517,6 +537,8 @@ extension SettingsViewController: UITableViewDelegate { sitePluginsWasPressed() case .support: supportWasPressed() + case .domain: + domainWasPressed() case .installJetpack: installJetpackWasPressed() case .privacy: @@ -592,6 +614,7 @@ extension SettingsViewController { case woocommerceDetails // Store settings + case domain case installJetpack // Help & Feedback @@ -637,6 +660,8 @@ extension SettingsViewController { return HostingTableViewCell.self case .support: return BasicTableViewCell.self + case .domain: + return BasicTableViewCell.self case .installJetpack: return BasicTableViewCell.self case .logout, .closeAccount: @@ -702,6 +727,11 @@ private extension SettingsViewController { comment: "Navigates to In-Person Payments screen" ) + static let domain = NSLocalizedString( + "Domain", + comment: "Navigates to domain settings screen." + ) + static let installJetpack = NSLocalizedString( "Install Jetpack", comment: "Navigates to Install Jetpack screen." diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift index c89dbb740b5..6453149e79b 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift @@ -221,6 +221,10 @@ private extension SettingsViewModel { let storeSettingsSection: Section? = { var rows: [Row] = [] + if featureFlagService.isFeatureFlagEnabled(.domainSettings) && stores.sessionManager.defaultSite?.isWordPressComStore == true { + rows.append(.domain) + } + if stores.sessionManager.defaultSite?.isJetpackCPConnected == true { rows.append(.installJetpack) } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 328296a3aba..ecc635db2d8 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -397,6 +397,8 @@ 02C27BCE282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C27BCD282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift */; }; 02C27BD0282CDF9E0065471A /* CardPresentPaymentReceiptEmailCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C27BCF282CDF9E0065471A /* CardPresentPaymentReceiptEmailCoordinatorTests.swift */; }; 02C37B79296694A900F0CF9E /* StoreCreationCategoryQuestionOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C37B78296694A900F0CF9E /* StoreCreationCategoryQuestionOptions.swift */; }; + 02C37B7B2967096800F0CF9E /* DomainSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C37B7A2967096800F0CF9E /* DomainSettingsView.swift */; }; + 02C37B7D2967B72A00F0CF9E /* FreeStagingDomainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C37B7C2967B72A00F0CF9E /* FreeStagingDomainView.swift */; }; 02C3FACE282A93020095440A /* WooAnalyticsEvent+Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C3FACD282A93020095440A /* WooAnalyticsEvent+Dashboard.swift */; }; 02C3FDEA251091CE009569EE /* ProductFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C3FDE9251091CE009569EE /* ProductFactoryTests.swift */; }; 02C8876D24501FAC00E4470F /* FilterListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C8876B24501FAC00E4470F /* FilterListViewController.swift */; }; @@ -423,6 +425,7 @@ 02DD81FA242CAA400060E50B /* Media+WPMediaAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD81F6242CAA3F0060E50B /* Media+WPMediaAsset.swift */; }; 02DD81FB242CAA400060E50B /* WordPressMediaLibraryPickerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD81F7242CAA3F0060E50B /* WordPressMediaLibraryPickerDataSource.swift */; }; 02DD81FC242CAA400060E50B /* WordPressMediaLibraryImagePickerViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02DD81F8242CAA400060E50B /* WordPressMediaLibraryImagePickerViewController.xib */; }; + 02DE39D92968647100BB31D4 /* DomainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE39D82968647100BB31D4 /* DomainSettingsViewModel.swift */; }; 02DE5CA9279F857D007CBEF3 /* Double+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE5CA8279F857D007CBEF3 /* Double+Rounding.swift */; }; 02DE5CAB279F8754007CBEF3 /* Double+RoundingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DE5CAA279F8754007CBEF3 /* Double+RoundingTests.swift */; }; 02DEA23328810B7A0057FC40 /* LoginOnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DEA23228810B7A0057FC40 /* LoginOnboardingScreen.swift */; }; @@ -2455,6 +2458,8 @@ 02C27BCD282CB52F0065471A /* CardPresentPaymentReceiptEmailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentReceiptEmailCoordinator.swift; sourceTree = ""; }; 02C27BCF282CDF9E0065471A /* CardPresentPaymentReceiptEmailCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentReceiptEmailCoordinatorTests.swift; sourceTree = ""; }; 02C37B78296694A900F0CF9E /* StoreCreationCategoryQuestionOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationCategoryQuestionOptions.swift; sourceTree = ""; }; + 02C37B7A2967096800F0CF9E /* DomainSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainSettingsView.swift; sourceTree = ""; }; + 02C37B7C2967B72A00F0CF9E /* FreeStagingDomainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeStagingDomainView.swift; sourceTree = ""; }; 02C3FACD282A93020095440A /* WooAnalyticsEvent+Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+Dashboard.swift"; sourceTree = ""; }; 02C3FDE9251091CE009569EE /* ProductFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFactoryTests.swift; sourceTree = ""; }; 02C8876B24501FAC00E4470F /* FilterListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterListViewController.swift; sourceTree = ""; }; @@ -2481,6 +2486,7 @@ 02DD81F6242CAA3F0060E50B /* Media+WPMediaAsset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Media+WPMediaAsset.swift"; sourceTree = ""; }; 02DD81F7242CAA3F0060E50B /* WordPressMediaLibraryPickerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressMediaLibraryPickerDataSource.swift; sourceTree = ""; }; 02DD81F8242CAA400060E50B /* WordPressMediaLibraryImagePickerViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = WordPressMediaLibraryImagePickerViewController.xib; sourceTree = ""; }; + 02DE39D82968647100BB31D4 /* DomainSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainSettingsViewModel.swift; sourceTree = ""; }; 02DE5CA8279F857D007CBEF3 /* Double+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Rounding.swift"; sourceTree = ""; }; 02DE5CAA279F8754007CBEF3 /* Double+RoundingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+RoundingTests.swift"; sourceTree = ""; }; 02DEA23228810B7A0057FC40 /* LoginOnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginOnboardingScreen.swift; sourceTree = ""; }; @@ -4523,6 +4529,9 @@ 023930602918F36400B2632F /* DomainSelectorView.swift */, 02393068291A065000B2632F /* DomainRowView.swift */, 02DAE7FB291B7B8B009342B7 /* DomainSelectorViewModel.swift */, + 02C37B7A2967096800F0CF9E /* DomainSettingsView.swift */, + 02C37B7C2967B72A00F0CF9E /* FreeStagingDomainView.swift */, + 02DE39D82968647100BB31D4 /* DomainSettingsViewModel.swift */, ); path = Domains; sourceTree = ""; @@ -10378,6 +10387,7 @@ D843D5D92248EE91001BFA55 /* ManualTrackingViewModel.swift in Sources */, B57B678A2107546E00AF8905 /* Address+Woo.swift in Sources */, 035F2308275690970019E1B0 /* CardPresentModalConnectingFailedUpdatePostalCode.swift in Sources */, + 02C37B7D2967B72A00F0CF9E /* FreeStagingDomainView.swift in Sources */, 457509E4267B9E91005FA2EA /* AggregatedProductListViewController.swift in Sources */, D8815B0D263861A400EDAD62 /* CardPresentModalSuccess.swift in Sources */, 0235595524496B6D004BE2B8 /* BottomSheetListSelectorCommand.swift in Sources */, @@ -10622,6 +10632,7 @@ B6E851F5276330200041D1BA /* RefundFeesDetailsTableViewCell.swift in Sources */, 028E1F702833DD0A001F8829 /* DashboardViewModel.swift in Sources */, 2602A63D27BD3C8C00B347F1 /* RemoteOrderSynchronizer.swift in Sources */, + 02C37B7B2967096800F0CF9E /* DomainSettingsView.swift in Sources */, CC4B252B27CFCEE2008D2E6E /* OrderTotalsCalculator.swift in Sources */, 0247F512286F73EA009C177E /* WooAnalyticsEvent+ImageUpload.swift in Sources */, B57C744A20F5649300EEFC87 /* EmptyStoresTableViewCell.swift in Sources */, @@ -10721,6 +10732,7 @@ 579CDEFF274D7E7900E8903D /* StoreStatsUsageTracksEventEmitter.swift in Sources */, 74F9E9CE214C036400A3F2D2 /* NoPeriodDataTableViewCell.swift in Sources */, 314DC4BD268D158F00444C9E /* CardReaderSettingsKnownReadersProvider.swift in Sources */, + 02DE39D92968647100BB31D4 /* DomainSettingsViewModel.swift in Sources */, 576EA39225264C7400AFC0B3 /* RefundConfirmationViewController.swift in Sources */, 2688641B25D3202B00821BA5 /* EditAttributesViewController.swift in Sources */, B50911302049E27A007D25DC /* DashboardViewController.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index 6bd13edd268..e85316aa439 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -10,6 +10,7 @@ struct MockFeatureFlagService: FeatureFlagService { private let isStoreCreationMVPEnabled: Bool private let isStoreCreationM2Enabled: Bool private let isStoreCreationM2WithInAppPurchasesEnabled: Bool + private let isDomainSettingsEnabled: Bool init(isInboxOn: Bool = false, isSplitViewInOrdersTabOn: Bool = false, @@ -18,7 +19,8 @@ struct MockFeatureFlagService: FeatureFlagService { isLoginPrologueOnboardingEnabled: Bool = false, isStoreCreationMVPEnabled: Bool = true, isStoreCreationM2Enabled: Bool = false, - isStoreCreationM2WithInAppPurchasesEnabled: Bool = false) { + isStoreCreationM2WithInAppPurchasesEnabled: Bool = false, + isDomainSettingsEnabled: Bool = false) { self.isInboxOn = isInboxOn self.isSplitViewInOrdersTabOn = isSplitViewInOrdersTabOn self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn @@ -27,6 +29,7 @@ struct MockFeatureFlagService: FeatureFlagService { self.isStoreCreationMVPEnabled = isStoreCreationMVPEnabled self.isStoreCreationM2Enabled = isStoreCreationM2Enabled self.isStoreCreationM2WithInAppPurchasesEnabled = isStoreCreationM2WithInAppPurchasesEnabled + self.isDomainSettingsEnabled = isDomainSettingsEnabled } func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool { @@ -47,6 +50,8 @@ struct MockFeatureFlagService: FeatureFlagService { return isStoreCreationM2Enabled case .storeCreationM2WithInAppPurchasesEnabled: return isStoreCreationM2WithInAppPurchasesEnabled + case .domainSettings: + return isDomainSettingsEnabled default: return false } diff --git a/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift index e9d208c6b2b..5fa244ca22e 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift @@ -130,6 +130,8 @@ private extension DomainSelectorViewModelTests { switch action { case let .loadFreeDomainSuggestions(_, completion): completion(.success(suggestions)) + default: + return } } } @@ -139,6 +141,8 @@ private extension DomainSelectorViewModelTests { switch action { case let .loadFreeDomainSuggestions(_, completion): completion(.failure(error)) + default: + return } } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/SettingsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/SettingsViewModelTests.swift index 2c7b033c243..14d98944922 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/SettingsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/SettingsViewModelTests.swift @@ -167,6 +167,54 @@ final class SettingsViewModelTests: XCTestCase { // Then XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.closeAccount) }) } + + func test_domain_is_hidden_when_domainSettings_feature_is_disabled() { + // Given + let featureFlagService = MockFeatureFlagService(isDomainSettingsEnabled: false) + stores.updateDefaultStore(.fake().copy(isWordPressComStore: true)) + let viewModel = SettingsViewModel(stores: stores, + storageManager: storageManager, + featureFlagService: featureFlagService, + appleIDCredentialChecker: appleIDCredentialChecker) + + // When + viewModel.onViewDidLoad() + + // Then + XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.domain) }) + } + + func test_domain_is_hidden_when_domainSettings_feature_is_enabled_and_site_is_not_wpcom() { + // Given + let featureFlagService = MockFeatureFlagService(isDomainSettingsEnabled: true) + sessionManager.defaultSite = .fake().copy(isWordPressComStore: false) + let viewModel = SettingsViewModel(stores: stores, + storageManager: storageManager, + featureFlagService: featureFlagService, + appleIDCredentialChecker: appleIDCredentialChecker) + + // When + viewModel.onViewDidLoad() + + // Then + XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.domain) }) + } + + func test_domain_is_shown_when_domainSettings_feature_is_enabled_and_site_is_wpcom() { + // Given + let featureFlagService = MockFeatureFlagService(isDomainSettingsEnabled: true) + sessionManager.defaultSite = .fake().copy(isWordPressComStore: true) + let viewModel = SettingsViewModel(stores: stores, + storageManager: storageManager, + featureFlagService: featureFlagService, + appleIDCredentialChecker: appleIDCredentialChecker) + + // When + viewModel.onViewDidLoad() + + // Then + XCTAssertTrue(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.domain) }) + } } private final class MockSettingsPresenter: SettingsViewPresenter { diff --git a/Yosemite/Yosemite/Actions/DomainAction.swift b/Yosemite/Yosemite/Actions/DomainAction.swift index 24982628d2d..09526aa86d2 100644 --- a/Yosemite/Yosemite/Actions/DomainAction.swift +++ b/Yosemite/Yosemite/Actions/DomainAction.swift @@ -4,4 +4,5 @@ import Foundation // public enum DomainAction: Action { case loadFreeDomainSuggestions(query: String, completion: (Result<[FreeDomainSuggestion], Error>) -> Void) + case loadDomains(siteID: Int64, completion: (Result<[SiteDomain], Error>) -> Void) } diff --git a/Yosemite/Yosemite/Actions/PaymentAction.swift b/Yosemite/Yosemite/Actions/PaymentAction.swift index 4d22d033a81..28703a4a7a8 100644 --- a/Yosemite/Yosemite/Actions/PaymentAction.swift +++ b/Yosemite/Yosemite/Actions/PaymentAction.swift @@ -10,6 +10,13 @@ public enum PaymentAction: Action { case loadPlan(productID: Int64, completion: (Result) -> Void) + /// Loads a site's current WPCOM plan. + /// - Parameters: + /// - siteID: The ID of a site. + /// - completion: Invoked when the site's current plan is loaded. + case loadSiteCurrentPlan(siteID: Int64, + completion: (Result) -> Void) + /// Creates a cart with a WPCOM plan. /// - Parameters: /// - productID: The ID of the WPCOM plan product. It is of string type to integrate with `InAppPurchasesForWPComPlansProtocol`. diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 73de4ab6dbe..e00fe1dc905 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -28,6 +28,7 @@ public typealias DotcomDevice = Networking.DotcomDevice public typealias DotcomUser = Networking.DotcomUser public typealias Feature = WordPressKit.Feature public typealias FreeDomainSuggestion = Networking.FreeDomainSuggestion +public typealias SiteDomain = Networking.SiteDomain public typealias InboxNote = Networking.InboxNote public typealias InboxAction = Networking.InboxAction public typealias JetpackUser = Networking.JetpackUser @@ -137,6 +138,7 @@ public typealias TopEarnerStatsItem = Networking.TopEarnerStatsItem public typealias User = Networking.User public typealias WooAPIVersion = Networking.WooAPIVersion public typealias WPComPlan = Networking.WPComPlan +public typealias WPComSitePlan = Networking.WPComSitePlan public typealias StoredProductSettings = Networking.StoredProductSettings public typealias CardReader = Hardware.CardReader public typealias CardReaderDiscoveryMethod = Hardware.CardReaderDiscoveryMethod diff --git a/Yosemite/Yosemite/Stores/DomainStore.swift b/Yosemite/Yosemite/Stores/DomainStore.swift index 49378684805..e3e4b8d3a7e 100644 --- a/Yosemite/Yosemite/Stores/DomainStore.swift +++ b/Yosemite/Yosemite/Stores/DomainStore.swift @@ -33,6 +33,8 @@ public final class DomainStore: Store { switch action { case .loadFreeDomainSuggestions(let query, let completion): loadFreeDomainSuggestions(query: query, completion: completion) + case .loadDomains(let siteID, let completion): + loadDomains(siteID: siteID, completion: completion) } } } @@ -44,4 +46,9 @@ private extension DomainStore { completion(result) } } + + func loadDomains(siteID: Int64, completion: @escaping (Result<[SiteDomain], Error>) -> Void) { + // TODO: 8558 - fetch a site's domains from the remote. + completion(.success([.init(name: "gotrees.wpcomstaging.com", isPrimary: true, renewalDate: nil)])) + } } diff --git a/Yosemite/Yosemite/Stores/PaymentStore.swift b/Yosemite/Yosemite/Stores/PaymentStore.swift index 050c7665d4a..87f910686a1 100644 --- a/Yosemite/Yosemite/Stores/PaymentStore.swift +++ b/Yosemite/Yosemite/Stores/PaymentStore.swift @@ -40,6 +40,8 @@ public final class PaymentStore: Store { switch action { case .loadPlan(let productID, let completion): loadPlan(productID: productID, completion: completion) + case .loadSiteCurrentPlan(let siteID, let completion): + loadSiteCurrentPlan(siteID: siteID, completion: completion) case .createCart(let productID, let siteID, let completion): createCart(productID: productID, siteID: siteID, completion: completion) } @@ -59,6 +61,12 @@ private extension PaymentStore { } } + func loadSiteCurrentPlan(siteID: Int64, + completion: (Result) -> Void) { + // TODO: 8558 - fetch site's current plan + completion(.success(.init(plan: .init(productID: 0, name: "", formattedPrice: ""), hasDomainCredit: true))) + } + func createCart(productID: String, siteID: Int64, completion: @escaping (Result) -> Void) {