From b3e873d9ea233b404b095864dc48d90ad060d9da Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 13:15:49 +1200 Subject: [PATCH 1/9] Remove the socialSharingV2 feature flag The Jetpack Social v2 experience is now always enabled. --- WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift | 4 ---- .../Blog/Blog Details/BlogDetailsViewController+Swift.swift | 4 +--- .../Post/PostSettings/CustomPostSettingsViewModel.swift | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index cd9caac4ee95..3057e96b9dc2 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -28,7 +28,6 @@ public enum FeatureFlag: Int, CaseIterable { case statsAds case customPostTypes case cptPostsAndPages - case socialSharingV2 case mediaLibraryV2 /// Returns a boolean indicating if the feature is enabled. @@ -91,8 +90,6 @@ public enum FeatureFlag: Int, CaseIterable { return BuildConfiguration.current == .debug case .cptPostsAndPages: return BuildConfiguration.current == .debug - case .socialSharingV2: - return BuildConfiguration.current == .debug case .mediaLibraryV2: return BuildConfiguration.current == .debug } @@ -139,7 +136,6 @@ extension FeatureFlag { case .statsAds: "Stats Ads Tab" case .customPostTypes: "Custom Post Types" case .cptPostsAndPages: "Custom Post Types: Posts and Pages" - case .socialSharingV2: "Social Sharing v2" case .mediaLibraryV2: "Media Library v2" } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift index de8e10e129c6..35449d705936 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift @@ -351,9 +351,7 @@ extension BlogDetailsViewController { if !blog.supports(.publicize) { // if publicize is disabled, show the sharing buttons settings. sharingVC = SharingButtonsViewController(blog: blog) - } else if FeatureFlag.socialSharingV2.enabled, - let manage = ManageConnectionsHostingController.make(for: blog) - { + } else if let manage = ManageConnectionsHostingController.make(for: blog) { sharingVC = manage } else { sharingVC = SharingViewController(blog: blog, delegate: nil) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift index e44bd5a3c52c..eb506fc16b92 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift @@ -604,7 +604,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting details: PostTypeDetailsWithEditContext ) -> SiteSocialConnectionsService? { guard PostSettingsCapabilities(from: details).supportsPublicize, - FeatureFlag.socialSharingV2.enabled, blog.supports(.publicize), let service = JetpackSocialFactory.shared.connectionsService(for: blog) else { From 568135c144c3724006695d131add1c2225d0cab9 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 14:38:36 +1200 Subject: [PATCH 2/9] Remove the unreachable legacy social sharing UI from post settings The v2 social sharing section fully shadows the legacy one: every condition that produced legacy content also produces the v2 binding, and PostSettings.sharing was already hardcoded to nil. --- .../AztecPostViewController.swift | 1 - .../Gutenberg/GutenbergViewController.swift | 1 - .../Post/PostEditor+JetpackSocial.swift | 9 - .../CustomPostSettingsViewModel.swift | 14 +- .../Post/PostSettings/PostSettings.swift | 78 ---- .../Post/PostSettings/PostSettingsView.swift | 26 +- .../PostSettings/PostSettingsViewModel.swift | 151 +------ .../PostSettingsViewModelProtocol.swift | 10 - .../Views/PrepublishingAutoSharingView.swift | 230 ---------- ...blishingSocialAccountsViewController.swift | 405 ------------------ 10 files changed, 6 insertions(+), 919 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift delete mode 100644 WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift delete mode 100644 WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 7094a993b266..9b05360d2865 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -457,7 +457,6 @@ class AztecPostViewController: UIViewController, PostEditor { addObservers(toPost: post) registerMediaObserver() - disableSocialConnectionsIfNecessary() } required init?(coder aDecoder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index b2fec133f27f..7ec478c3d234 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -321,7 +321,6 @@ class GutenbergViewController: UIViewController, PostEditor, PublishingEditor { super.init(nibName: nil, bundle: nil) self.navigationBarManager.delegate = self - disableSocialConnectionsIfNecessary() } required init?(coder aDecoder: NSCoder) { diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift deleted file mode 100644 index 836e83064bc8..000000000000 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+JetpackSocial.swift +++ /dev/null @@ -1,9 +0,0 @@ -import WordPressData - -extension PostEditor { - - // Deprecated: Jetpack Social no longer enforces per-post share limits, and post editing now uses - // connection_id-keyed PostSocialSharingDraft metadata instead of keyring publicize state. - func disableSocialConnectionsIfNecessary() { - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift index eb506fc16b92..6fc27c5a0b58 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/CustomPostSettingsViewModel.swift @@ -39,7 +39,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting @Published var suggestedTags: [String] = [] @Published var customTaxonomies: [SiteTaxonomy] = [] @Published var parentPageText: String? - @Published var socialSharingState: PostSettingsSocialSharingSectionState? @Published var isShowingDeletedAlert = false private let socialConnectionsService: SiteSocialConnectionsService? @@ -50,7 +49,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting private var addConnectionCoordinator: AddConnectionCoordinator? private let originalSettings: PostSettings - private let preferences: UserPersistentRepository private var cancellables = Set() var onDismiss: (() -> Void)? @@ -199,14 +197,12 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting blog: Blog, socialConnectionsService: SiteSocialConnectionsService?, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.editorService = editorService self.blog = blog self.isStandalone = isStandalone self.context = context - self.preferences = preferences self.client = editorService.client let capabilities = PostSettingsCapabilities(from: editorService.details) self.capabilities = capabilities @@ -277,16 +273,14 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting editorService: CustomPostEditorService, blog: Blog, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.init( editorService: editorService, blog: blog, socialConnectionsService: Self.resolveSocialConnectionsService(blog: blog, details: editorService.details), isStandalone: isStandalone, - context: context, - preferences: preferences + context: context ) } @@ -401,8 +395,6 @@ final class CustomPostSettingsViewModel: NSObject, ObservableObject, PostSetting viewController?.navigationController?.pushViewController(categoriesVC, animated: true) } - func showSocialSharingOptions() {} - var v2SocialSharing: V2SocialSharingBinding? { guard let service = socialConnectionsService, settings.status != .publishPrivate diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift index 8244ae94eeb0..6e46e9c0d418 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettings.swift @@ -65,7 +65,6 @@ struct PostSettings: Hashable { // MARK: - Post-specific var postFormat: String? var isStickyPost = false - var sharing: PostSocialSharingSettings? /// Publicize draft state used for change detection and REST parameter encoding. /// When available, `connectionsByID` carries the full post-level connection state. var socialSharingDraft: PostSocialSharingDraft? @@ -125,7 +124,6 @@ struct PostSettings: Hashable { if PostSocialSharing.isEligible(for: post) { socialSharingDraft = PostSocialSharingDraft(socialMetadata: PostMetadataContainer(post)) } - // `sharing` (the legacy keyring-keyed model) is intentionally no longer populated. allowComments = post.commentsStatus.map { $0 != RemotePostDiscussionState.closed.rawValue } allowPings = post.pingsStatus.map { $0 != RemotePostDiscussionState.closed.rawValue } case let page as Page: @@ -183,9 +181,6 @@ struct PostSettings: Hashable { parentPageID = post.parent.map { Int($0) } - // Legacy Publicize settings are not available for REST API posts. - sharing = nil - let socialSharingDraft = PostSocialSharingDraft( fromPostAdditionalFields: post.additionalFields, meta: post.meta @@ -642,76 +637,3 @@ extension PostStatus { } } } - -/// A value-type representation of `PublicizeService` for the current blog that's simplified for the auto-sharing flow. -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -struct PostSocialSharingSettings: Hashable { - var services: [Service] - var message: String - var sharingLimit: PublicizeInfo.SharingLimit? - - struct Service: Hashable { - let name: PublicizeService.ServiceName - var connections: [Connection] - } - - struct Connection: Hashable { - let account: String - let keyringID: Int - var enabled: Bool - } - - static func make(for post: Post) -> PostSocialSharingSettings? { - guard let context = post.managedObjectContext else { - wpAssertionFailure("missing moc") - return nil - } - - let connections = post.blog.sortedConnections - - // first, build a dictionary to categorize the connections. - var connectionsMap = [PublicizeService.ServiceName: [PublicizeConnection]]() - connections.filter { !$0.requiresUserAction() } - .forEach { connection in - let name = PublicizeService.ServiceName(rawValue: connection.service) ?? .unknown - var serviceConnections = connectionsMap[name] ?? [] - serviceConnections.append(connection) - connectionsMap[name] = serviceConnections - } - - let publicizeServices: [PublicizeService] - do { - publicizeServices = try PublicizeService.allPublicizeServices(in: context) - } catch { - wpAssertionFailure("failed to fetch services", userInfo: ["error": error.localizedDescription]) - return nil - } - - let services = publicizeServices.compactMap { service -> PostSocialSharingSettings.Service? in - // skip services without connections. - guard let serviceConnections = connectionsMap[service.name], - !serviceConnections.isEmpty - else { - return nil - } - - return PostSocialSharingSettings.Service( - name: service.name, - connections: serviceConnections.map { - .init( - account: $0.externalDisplay, - keyringID: $0.keyringConnectionID.intValue, - enabled: !post.publicizeConnectionDisabledForKeyringID($0.keyringConnectionID) - ) - } - ) - } - - return PostSocialSharingSettings( - services: services, - message: post.publicizeMessage ?? post.titleForDisplay(), - sharingLimit: post.blog.sharingLimit - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift index 135dc01b0c97..58f10c8cdbb0 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsView.swift @@ -153,7 +153,7 @@ struct PostSettingsFormContentView: Vi excerptSection } generalSection - socialSharingSectionDispatcher + socialSharingSection accessSection moreOptionsSection } @@ -383,7 +383,7 @@ struct PostSettingsFormContentView: Vi // MARK: - "Social Sharing" Section @ViewBuilder - private var socialSharingSectionDispatcher: some View { + private var socialSharingSection: some View { if let binding = viewModel.v2SocialSharing { // The server early-returns updates to `jetpack_publicize_connections` // for already-published posts, and re-share isn't supported in the @@ -395,28 +395,6 @@ struct PostSettingsFormContentView: Vi onAddConnection: binding.onAddConnection ) } - } else { - socialSharingSection - } - } - - @ViewBuilder - private var socialSharingSection: some View { - if let state = viewModel.socialSharingState { - Section { - switch state { - case .setup(let viewModel): - JetpackSocialNoConnectionView(viewModel: viewModel) - case .connected: - if let settings = viewModel.settings.sharing { - LegacyNavigationLinkRow(action: viewModel.showSocialSharingOptions) { - PrepublishingAutoSharingView(model: settings) - } - } - } - } header: { - SectionHeader(Strings.socialSharing) - } } } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift index 636dad8261e7..2f66e9874b40 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModel.swift @@ -36,7 +36,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM @Published private(set) var suggestedTags: [String] = [] @Published private(set) var customTaxonomies: [SiteTaxonomy] = [] @Published private(set) var parentPageText: String? - @Published private(set) var socialSharingState: PostSettingsSocialSharingSectionState? @Published var isShowingDeletedAlert = false @@ -170,7 +169,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM } private let originalSettings: PostSettings - private let preferences: UserPersistentRepository private var isSuggestedTagsRefreshNeeded = true private var cancellables = Set() @@ -187,15 +185,13 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM init( post: AbstractPost, isStandalone: Bool = false, - context: PostSettingsContext = .settings, - preferences: UserPersistentRepository = UserDefaults.standard + context: PostSettingsContext = .settings ) { self.post = post self.blog = post.blog self.capabilities = post is Post ? .post() : .page() self.isStandalone = isStandalone self.context = context - self.preferences = preferences self.client = try? WordPressClientFactory.shared.instance(for: .init(blog: post.blog)) self.socialConnectionsService = Self.resolveSocialConnectionsService(for: post) @@ -221,7 +217,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM refreshDisplayedTags() refreshCustomTaxonomies() refreshParentPageText() - refreshSocialSharingState() resolveAbstractPostTerms() WPAnalytics.track(.postSettingsShown) @@ -332,9 +327,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM if old.parentPageID != new.parentPageID { refreshParentPageText() } - if old.status != new.status { - refreshSocialSharingState() - } } private func refreshDisplayedCategories() { @@ -544,110 +536,6 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM coordinator.start() } - private func refreshSocialSharingState() { - guard settings.status != .publishPrivate, - socialConnectionsService != nil, - let post = post as? Post, - isPostEligibleForSocialSharing(post) - else { - socialSharingState = nil - return - } - if (blog.connections ?? []).isEmpty { - if isSocialConnectionSetupDismissed { - socialSharingState = nil - } else { - socialSharingState = .setup(makeSocialSharingSetupViewModel()) - } - } else { - socialSharingState = .connected - } - } - - private func isPostEligibleForSocialSharing(_ post: Post) -> Bool { - BuildSettings.current.brand == .jetpack - && RemoteFeatureFlag.jetpackSocialImprovements.enabled() - && post.status != .publishPrivate - && !getPublicizeServices().isEmpty - && blog.supports(.publicize) - } - - private func getPublicizeServices() -> [PublicizeService] { - let context = ContextManager.shared.mainContext - return (try? PublicizeService.allSupportedServices(in: context)) ?? [] - } - - /// Convenience variable representing whether the No Connection view has been dismissed. - /// Note: the value is stored per site. - private var isSocialConnectionSetupDismissed: Bool { - get { - guard let blogID = blog.dotComID?.intValue, - let dictionary = preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool], - let value = dictionary["\(blogID)"] - else { - return false - } - return value - } - set { - guard let blogID = blog.dotComID?.intValue else { - return wpAssertionFailure("blogID missing") - } - var dictionary = (preferences.dictionary(forKey: Constants.noConnectionKey) as? [String: Bool]) ?? .init() - dictionary["\(blogID)"] = newValue - preferences.set(dictionary, forKey: Constants.noConnectionKey) - } - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func makeSocialSharingSetupViewModel() -> JetpackSocialNoConnectionViewModel { - JetpackSocialNoConnectionViewModel( - services: getPublicizeServices(), - padding: .zero, - onConnectTap: { [weak self] in self?.showSocialSharingSetupScreen() }, - onNotNowTap: { [weak self] in self?.didDismissSocialSharingSetupPrompt() } - ) - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func showSocialSharingSetupScreen() { - guard let sharingVC = SharingViewController(blog: blog, delegate: self) else { - return wpAssertionFailure("failed to instantiate SharingVC") - } - track(.jetpackSocialNoConnectionCTATapped) - let navigationVC = UINavigationController(rootViewController: sharingVC) - viewController?.present(navigationVC, animated: true) - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - private func didDismissSocialSharingSetupPrompt() { - track(.jetpackSocialNoConnectionCardDismissed) - isSocialConnectionSetupDismissed = true - withAnimation { - socialSharingState = nil - } - } - - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept for remaining legacy references. - func showSocialSharingOptions() { - guard let blogID = blog.dotComID?.intValue, - let settings = settings.sharing - else { - return wpAssertionFailure("invalid context") - } - let optionsVC = PrepublishingSocialAccountsViewController( - blogID: blogID, - model: settings, - delegate: self, - coreDataStack: ContextManager.shared - ) - viewController?.navigationController?.pushViewController(optionsVC, animated: true) - } - // MARK: - Navigation func showCategoriesPicker() { @@ -720,40 +608,3 @@ final class PostSettingsViewModel: NSObject, ObservableObject, PostSettingsViewM } } } - -extension PostSettingsViewModel: @MainActor SharingViewControllerDelegate { - func didChangePublicizeServices() { - refreshSocialSharingState() - } -} - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -extension PostSettingsViewModel: @MainActor PrepublishingSocialAccountsDelegate { - func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) { - settings.sharing?.sharingLimit = newValue - } - - func didFinish(with connectionChanges: [Int: Bool], message: String?) { - guard var settings = settings.sharing else { - return wpAssertionFailure("social sharing settings missing") - } - settings.services = settings.services.map { - var service = $0 - service.connections = service.connections.map { - var connection = $0 - if let isEnabled = connectionChanges[connection.keyringID] { - connection.enabled = isEnabled - } - return connection - } - return service - } - settings.message = message ?? "" - self.settings.sharing = settings - } -} - -private enum Constants { - static let noConnectionKey = "prepublishing-social-no-connection-view-hidden" -} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift index 84d4504b7e5c..392a3fe4dddb 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettings/PostSettingsViewModelProtocol.swift @@ -34,14 +34,6 @@ enum PostSettingsMoreOption: Identifiable { var id: Self { self } } -/// The state of the social sharing section. -enum PostSettingsSocialSharingSectionState { - /// The initial prompt to set up connections. - case setup(JetpackSocialNoConnectionViewModel) - /// The site has existing connections. - case connected -} - // MARK: - Protocol /// Defines the contract for a post settings view model consumed by `PostSettingsView` @@ -69,7 +61,6 @@ protocol PostSettingsViewModelProtocol: ObservableObject { var suggestedTags: [String] { get } var customTaxonomies: [SiteTaxonomy] { get } var parentPageText: String? { get } - var socialSharingState: PostSettingsSocialSharingSectionState? { get } /// Non-nil when the host screen should render the v2 social sharing /// section. Custom post settings and eligible `AbstractPost` settings @@ -117,7 +108,6 @@ protocol PostSettingsViewModelProtocol: ObservableObject { func didSelectSuggestedTag(_ tag: String) func didSelectTags(_ tags: [TagsViewModel.SelectedTerm]) func didSelectTerms(_ terms: [TagsViewModel.SelectedTerm], forTaxonomySlug: String) - func showSocialSharingOptions() func showCategoriesPicker() func parentPagePickerDestination() -> ParentPagePickerDestination? } diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift deleted file mode 100644 index 800d9fb3ef80..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingAutoSharingView.swift +++ /dev/null @@ -1,230 +0,0 @@ -import SwiftUI -import WordPressUI - -struct PrepublishingAutoSharingView: View { - - let model: PostSocialSharingSettings - - @Environment(\.sizeCategory) private var sizeCategory - - @ScaledMetric(relativeTo: .subheadline) private var warningIconLength = 16.0 - - @ScaledMetric(relativeTo: .body) private var imageLength = 28.0 - - var body: some View { - Group { - if shouldStackContentVertically { - VStack(alignment: .leading, spacing: 8.0) { content } - } else { - HStack { content } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityElement(children: .combine) - .accessibilityAddTraits(.isButton) - } - - private var content: some View { - Group { - textStack - if !model.services.isEmpty { - if !shouldStackContentVertically { - Spacer(minLength: .zero) // to push the icons to the trailing side in horizontal layout. - } - socialIconsView - } - } - } - - private var textStack: some View { - VStack(alignment: .leading, spacing: 2.0) { - Text(model.labelText) - .font(.body) - .foregroundColor(Color(.label)) - if let text = model.remainingSharesText { - remainingSharesLabel(text: text, showsWarning: model.showsWarning) - } - } - .lineLimit(1) - } - - @ViewBuilder - private func remainingSharesLabel(text: String, showsWarning: Bool) -> some View { - Label { - Text(text) - .font(.subheadline) - .foregroundColor(Color(showsWarning ? Constants.warningColor : .secondaryLabel)) - } icon: { - if showsWarning { - Image("icon-warning") - .resizable() - .frame(width: warningIconLength, height: warningIconLength) - .accessibilityElement() - .accessibilityLabel(Constants.warningIconAccessibilityText) - } - } - .accessibilityLabel(showsWarning ? "\(Constants.warningIconAccessibilityText), \(text)" : text) - } - - private var socialIconsView: some View { - HStack(spacing: -6) { - ForEach(model.services, id: \.self) { service in - iconImage(service.name.localIconImage, opaque: service.usesOpaqueIcon) - } - } - } - - private func iconImage(_ uiImage: UIImage, opaque: Bool) -> some View { - Image(uiImage: uiImage) - .resizable() - .frame(width: imageLength, height: imageLength) - .opacity(opaque ? 1.0 : Constants.disabledSocialIconOpacity) - .background(Color(.secondarySystemGroupedBackground)) - .clipShape(Circle()) - } -} - -// MARK: - View Helpers - -private extension PrepublishingAutoSharingView { - - var shouldStackContentVertically: Bool { - (model.services.count > Constants.maxServicesForHorizontalLayout) || sizeCategory.isAccessibilityCategory - } - - enum Constants { - static let maxServicesForHorizontalLayout = 3 - static let disabledSocialIconOpacity: CGFloat = 0.36 - static let warningColor = UIAppColor.yellow(.shade50) - - static let warningIconAccessibilityText = NSLocalizedString( - "prepublishing.social.warningIcon.accessibilityHint", - value: "Warning", - comment: "a VoiceOver description for the warning icon to hint that the remaining shares are low." - ) - } -} - -// MARK: - PrepublishingAutoSharingModel Private Extensions - -private extension PostSocialSharingSettings.Service { - /// Whether the icon for this service should be opaque or transparent. - /// If at least one account is enabled, an opaque version should be shown. - var usesOpaqueIcon: Bool { - connections.reduce(false) { partialResult, connection in - return partialResult || connection.enabled - } - } - - var enabledConnections: [PostSocialSharingSettings.Connection] { - connections.filter { $0.enabled } - } -} - -private extension PostSocialSharingSettings { - var enabledConnectionsCount: Int { - services.reduce(0) { $0 + $1.enabledConnections.count } - } - - var totalConnectionsCount: Int { - services.reduce(0) { $0 + $1.connections.count } - } - - var showsWarning: Bool { - guard let remaining = sharingLimit?.remaining else { - return false - } - return totalConnectionsCount > remaining - } - - var labelText: String { - switch (enabledConnectionsCount, totalConnectionsCount) { - case (let enabled, _) where enabled == 0: - // not sharing to any social media - return Strings.notSharingText - - case (let enabled, let total) where enabled == total && total == 1: - // sharing to the one and only connection - guard let account = services.first?.connections.first?.account else { - return String() - } - return String(format: Strings.singleConnectionTextFormat, account) - - case (let enabled, let total) where enabled == total && total > 1: - // sharing to all connections - return String(format: Strings.multipleConnectionsTextFormat, enabled) - - case (let enabled, let total) where enabled < total && total > 1: - // sharing to some connections - return String(format: Strings.partialConnectionsTextFormat, enabled, total) - - default: - return String() - } - } - - var remainingSharesText: String? { - guard let remaining = sharingLimit?.remaining else { - return nil - } - - return String(format: Strings.remainingSharesTextFormat, remaining) - } - - enum Strings { - static let notSharingText = NSLocalizedString( - "prepublishing.social.label.notSharing", - value: "Not sharing to social", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the blog post will not be shared to any social accounts. - """ - ) - - static let singleConnectionTextFormat = NSLocalizedString( - "prepublishing.social.label.singleConnection", - value: "Sharing to %1$@", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the blog post will be shared to a social media account. - %1$@ is a placeholder for the account name. - Example: Sharing to @wordpress - """ - ) - - static let multipleConnectionsTextFormat = NSLocalizedString( - "prepublishing.social.label.multipleConnections", - value: "Sharing to %1$d accounts", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the number of social accounts that will be sharing the blog post. - %1$d is a placeholder for the number of social network accounts that will be auto-shared. - Example: Sharing to 3 accounts - """ - ) - - static let partialConnectionsTextFormat = NSLocalizedString( - "prepublishing.social.label.partialConnections", - value: "Sharing to %1$d of %2$d accounts", - comment: """ - The primary label for the auto-sharing row on the pre-publishing sheet. - Indicates the number of social accounts that will be sharing the blog post. - This string is displayed when some of the social accounts are turned off for auto-sharing. - %1$d is a placeholder for the number of social media accounts that will be sharing the blog post. - %2$d is a placeholder for the total number of social media accounts connected to the user's blog. - Example: Sharing to 2 of 3 accounts - """ - ) - - static let remainingSharesTextFormat = NSLocalizedString( - "prepublishing.social.remainingShares.format", - value: "%1$d social shares remaining", - comment: """ - A subtext that's shown below the primary label in the auto-sharing row on the pre-publishing sheet. - Informs the remaining limit for post auto-sharing. - %1$d is a placeholder for the remaining shares. - Example: 27 social shares remaining - """ - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift deleted file mode 100644 index 3cc7b3c13ec3..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsViewController.swift +++ /dev/null @@ -1,405 +0,0 @@ -import UIKit -import SwiftUI -import WordPressData -import WordPressShared -import WordPressUI - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -protocol PrepublishingSocialAccountsDelegate: NSObjectProtocol { - - func didUpdateSharingLimit(with newValue: PublicizeInfo.SharingLimit?) - - func didFinish(with connectionChanges: [Int: Bool], message: String?) -} - -// Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. -// Kept for remaining legacy references. -class PrepublishingSocialAccountsViewController: UITableViewController { - - // MARK: Properties - - private let coreDataStack: CoreDataStackSwift - - private let service: BlogService - - private weak var delegate: PrepublishingSocialAccountsDelegate? - - private let blogID: Int - - private let connections: [Connection] - - private let originalMessage: String - - private var connectionChanges = [Int: Bool]() - - private var sharingLimit: PublicizeInfo.SharingLimit? { - didSet { - toggleInteractivityIfNeeded() - tableView.reloadData() - delegate?.didUpdateSharingLimit(with: sharingLimit) - } - } - - private var shareMessage: String { - didSet { - tableView.reloadData() - } - } - - /// Stores the interaction state for disabled connections. - /// The value is stored in order to perform table operations *only* when the value changes. - private var canInteractWithDisabledConnections: Bool { - didSet { - guard oldValue != canInteractWithDisabledConnections else { - return - } - // only reload connections that are turned off. - // the last toggled row is skipped so it can perform its full switch animation. - tableView.reloadRows(at: indexPathsForDisabledConnections.filter { $0.row != lastToggledRow }, with: .none) - lastToggledRow = -1 // reset once the reload completes. - } - } - - /// Stores the last table row toggled by the user. - /// - /// This property is only used for visual purposes, to allow the toggled cell's switch animation to complete - /// instead of having it abruptly stopped due to the table view reload. - private var lastToggledRow: Int = -1 - - // MARK: Methods - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - init( - blogID: Int, - model: PostSocialSharingSettings, - delegate: PrepublishingSocialAccountsDelegate?, - coreDataStack: CoreDataStackSwift = ContextManager.shared, - blogService: BlogService? = nil - ) { - self.blogID = blogID - self.connections = model.services.flatMap { service in - service.connections.map { - .init(service: service.name, account: $0.account, keyringID: $0.keyringID, isOn: $0.enabled) - } - } - self.originalMessage = model.message - self.shareMessage = originalMessage - self.sharingLimit = model.sharingLimit - self.delegate = delegate - self.coreDataStack = coreDataStack - self.service = blogService ?? BlogService(coreDataStack: coreDataStack) - self.canInteractWithDisabledConnections = model.enabledConnectionsCount < (sharingLimit?.remaining ?? .max) - - super.init(style: .insetGrouped) - } - - override func viewDidLoad() { - super.viewDidLoad() - - title = Strings.navigationTitle - - tableView.register(SwitchTableViewCell.self, forCellReuseIdentifier: Constants.accountCellIdentifier) - tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.messageCellIdentifier) - - // setting a custom spacer view will override the default 34pt padding from the grouped table view style. - tableView.tableHeaderView = UIView(frame: .init(x: 0, y: 0, width: 0, height: Constants.tableTopPadding)) - } - - deinit { - // only call the delegate method if the user has made some changes. - if hasChanges { - delegate?.didFinish(with: connectionChanges, message: shareMessage) - } - } -} - -// MARK: - UITableView - -extension PrepublishingSocialAccountsViewController { - - override func numberOfSections(in tableView: UITableView) -> Int { - 2 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case 0: connections.count - case 1: 1 - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch indexPath.section { - case 0: - return accountCell(for: indexPath) - case 1: - let cell = tableView.dequeueReusableCell(withIdentifier: Constants.messageCellIdentifier, for: indexPath) - cell.contentConfiguration = UIHostingConfiguration { - SocialSharingMessageRow(text: shareMessage) - } - cell.accessoryType = .disclosureIndicator - return cell - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // interactions for the account switches are absorbed by the tap gestures set up in the SwitchTableViewCell, - // so it shouldn't trigger this method. In any case, we should only care about handling taps on the message row. - switch indexPath.section { - case 0: break // Do nothing - case 1: showEditMessageScreen() - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch section { - case 0: Strings.servicesSectionTitle - case 1: Strings.messageCellLabelText - default: fatalError("invalid section") - } - } - - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - guard let sharingLimit else { - return nil - } - - return PrepublishingSocialAccountsTableFooterView( - remaining: sharingLimit.remaining, - showsWarning: shouldDisplayWarning, - onButtonTap: { [weak self] in - self?.subscribeButtonTapped() - } - ) - } -} - -// MARK: - Private Helpers - -private extension PrepublishingSocialAccountsViewController { - var enabledCount: Int { - connections - .filter { connectionChanges[$0.keyringID] ?? $0.isOn } - .count - } - - var indexPathsForDisabledConnections: [IndexPath] { - connections.indices.compactMap { index in - valueForConnection(at: index) ? nil : IndexPath(row: index, section: .zero) - } - } - - var shouldDisplayWarning: Bool { - connections.count >= (sharingLimit?.remaining ?? .max) - } - - var hasChanges: Bool { - !connectionChanges.isEmpty || shareMessage != originalMessage - } - - func accountCell(for indexPath: IndexPath) -> UITableViewCell { - guard var connection = connections[safe: indexPath.row], - let cell = tableView.dequeueReusableCell(withIdentifier: Constants.accountCellIdentifier) - as? SwitchTableViewCell - else { - return UITableViewCell() - } - - cell.textLabel?.text = connection.account - cell.textLabel?.numberOfLines = 1 - cell.textLabel?.adjustsFontForContentSizeCategory = true - cell.imageView?.image = connection.imageForCell - cell.on = valueForConnection(at: indexPath.row) - cell.onChange = { [weak self] newValue in - self?.updateConnection(at: indexPath.row, value: newValue) - } - - let isInteractionAllowed = cell.on || canInteractWithDisabledConnections - isInteractionAllowed ? cell.enable() : cell.disable() - cell.imageView?.alpha = isInteractionAllowed ? 1.0 : Constants.disabledCellImageOpacity - - cell.accessibilityLabel = "\(connection.service.description), \(connection.account)" - - return cell - } - - func valueForConnection(at index: Int) -> Bool { - guard let connection = connections[safe: index] else { - return false - } - return connectionChanges[connection.keyringID] ?? connection.isOn - } - - func updateConnection(at index: Int, value: Bool) { - guard let connection = connections[safe: index] else { - return - } - - let originalValue = connection.isOn - - if value == originalValue { - connectionChanges.removeValue(forKey: connection.keyringID) - } else { - connectionChanges[connection.keyringID] = value - } - - lastToggledRow = index - toggleInteractivityIfNeeded() - - WPAnalytics.track( - .jetpackSocialConnectionToggled, - properties: ["source": Constants.trackingSource, "value": value] - ) - } - - func toggleInteractivityIfNeeded() { - canInteractWithDisabledConnections = enabledCount < (sharingLimit?.remaining ?? .max) - } - - func showEditMessageScreen() { - let multiTextViewController = SettingsMultiTextViewController( - text: shareMessage, - placeholder: nil, - hint: Strings.editShareMessageHint, - isPassword: false - ) - - multiTextViewController.title = Strings.editShareMessageNavigationTitle - multiTextViewController.onValueChanged = { [weak self] newValue in - self?.shareMessage = newValue - } - - self.navigationController?.pushViewController(multiTextViewController, animated: true) - } - - func subscribeButtonTapped() { - guard let checkoutViewController = makeCheckoutViewController() else { - return - } - - WPAnalytics.track(.jetpackSocialUpgradeLinkTapped, properties: ["source": Constants.trackingSource]) - - let navigationController = UINavigationController(rootViewController: checkoutViewController) - show(navigationController, sender: nil) - } - - func makeCheckoutViewController() -> UIViewController? { - coreDataStack.performQuery { [weak self] context in - guard let self, - let blog = try? Blog.lookup(withID: self.blogID, in: context), - let host = blog.hostname, - let url = URL(string: "https://wordpress.com/checkout/\(host)/jetpack_social_basic_yearly") - else { - return nil - } - - return WebViewControllerFactory.controller(url: url, blog: blog, source: Constants.webViewSource) { - self.checkoutDismissed() - } - } - } - - /// When the checkout web view is dismissed, try to sync the latest sharing limit in case the user did make - /// a purchase. We can make this assumption if the returned `sharingLimit` is nil, which means there's no longer - /// any sharing limit for the site. - func checkoutDismissed() { - assert(Thread.isMainThread, "\(#function) must be called from the main thread") - - guard let blog = try? Blog.lookup(withID: blogID, in: coreDataStack.mainContext), - ReachabilityUtils.isInternetReachable() - else { - return - } - - service.syncBlog(blog) { [weak self] in - guard let self else { - return - } - - // re-fetch the blog after sync completes to check if the sharing limit for the blog has been removed. - self.sharingLimit = self.coreDataStack.performQuery { context in - guard let blog = try? Blog.lookup(withID: self.blogID, in: context) else { - return nil - } - return blog.sharingLimit - } - } failure: { error in - DDLogError("Failed to sync blog after dismissing checkout webview due to error: \(error)") - } - } - - /// Convenient model that represents the user's Publicize connections. - struct Connection { - let service: PublicizeService.ServiceName - let account: String - let keyringID: Int - let isOn: Bool - - lazy var imageForCell: UIImage = { - service.localIconImage.resized(to: Constants.cellImageSize, format: .scaleAspectFit) - }() - } - - // MARK: Constants - - enum Constants { - static let disabledCellImageOpacity = 0.36 - static let cellImageSize = CGSize(width: 28.0, height: 28.0) - - static let tableTopPadding: CGFloat = 16.0 - - static let accountCellIdentifier = "AccountCell" - static let messageCellIdentifier = "MessageCell" - - static let webViewSource = "prepublishing_social_accounts_subscribe" - static let trackingSource = "pre_publishing" - } -} - -private enum Strings { - static let navigationTitle = NSLocalizedString( - "prepublishing.socialAccounts.navigationTitle", - value: "Social", - comment: "The navigation title for the pre-publishing social accounts screen." - ) - - static let servicesSectionTitle = NSLocalizedString( - "prepublishing.socialAccounts.servicesSectionTitle", - value: "Services", - comment: "Table section title" - ) - - static let messageCellLabelText = NSLocalizedString( - "prepublishing.socialAccounts.message.label", - value: "Message", - comment: "Table section title" - ) - - static let editShareMessageNavigationTitle = NSLocalizedString( - "prepublishing.socialAccounts.editMessage.navigationTitle", - value: "Customize message", - comment: "The navigation title for a screen that edits the sharing message for the post." - ) - - static let editShareMessageHint = NSLocalizedString( - "prepublishing.socialAccounts.editMessage.hint", - value: """ - Customize the message you want to share. - If you don't add your own text here, we'll use the post's title as the message. - """, - comment: "A hint shown below the text field when editing the sharing message from the pre-publishing flow." - ) -} - -private extension PostSocialSharingSettings { - var enabledConnectionsCount: Int { - services.flatMap { $0.connections }.filter { $0.enabled }.count - } -} From fc3574c50f68095f144af63a374642b9be889546 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 14:52:16 +1200 Subject: [PATCH 3/9] Apply swift-format to files touched by Task 2 --- WordPress/Classes/Utility/ObjCBridge.swift | 7 +- ...SiteStatsInsightsTableViewController.swift | 247 +++++++++++------- 2 files changed, 165 insertions(+), 89 deletions(-) diff --git a/WordPress/Classes/Utility/ObjCBridge.swift b/WordPress/Classes/Utility/ObjCBridge.swift index 8f95ee07cb8a..3768cbbc0c97 100644 --- a/WordPress/Classes/Utility/ObjCBridge.swift +++ b/WordPress/Classes/Utility/ObjCBridge.swift @@ -15,7 +15,12 @@ import WordPressData SupportTableViewController().showFromTabBar() } - @objc public class func makeSharingAuthorizationViewController(publicizer: PublicizeService, url: URL, blog: Blog, delegate: SharingAuthorizationDelegate) -> UIViewController { + @objc public class func makeSharingAuthorizationViewController( + publicizer: PublicizeService, + url: URL, + blog: Blog, + delegate: SharingAuthorizationDelegate + ) -> UIViewController { SharingAuthorizationWebViewController(with: publicizer, url: url, for: blog, delegate: delegate) } diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift index eda76d5720cd..346c287c62a2 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift @@ -8,7 +8,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { weak var bannerView: JetpackBannerView? var isGrowAudienceShowing: Bool { - return insightsToShow.contains(.growAudience) + insightsToShow.contains(.growAudience) } private var insightsChangeReceipt: Receipt? @@ -41,7 +41,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private var displayingEmptyView = false private lazy var mainContext: NSManagedObjectContext = { - return ContextManager.shared.mainContext + ContextManager.shared.mainContext }() private var viewModel: SiteStatsInsightsViewModel? @@ -49,7 +49,7 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private let analyticsTracker = BottomScrollAnalyticsTracker() private lazy var tableHandler: ImmuTableDiffableViewHandler = { - return ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker) + ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker) }() // MARK: - View @@ -109,8 +109,11 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { dismissCustomizeCard() } - let controller = InsightsManagementViewController(insightsDelegate: self, - insightsManagementDelegate: self, insightsShown: insightsToShow.compactMap { $0.statSection }) + let controller = InsightsManagementViewController( + insightsDelegate: self, + insightsManagementDelegate: self, + insightsShown: insightsToShow.compactMap { $0.statSection } + ) let navigationController = UINavigationController(rootViewController: controller) navigationController.modalPresentationStyle = .formSheet // Has to be set before the delegate navigationController.presentationController?.delegate = self @@ -123,11 +126,13 @@ class SiteStatsInsightsTableViewController: SiteStatsBaseTableViewController { private extension SiteStatsInsightsTableViewController { func initViewModel() { - viewModel = SiteStatsInsightsViewModel(insightsToShow: insightsToShow, - insightsDelegate: self, - viewsAndVisitorsDelegate: self, - insightsStore: insightsStore, - pinnedItemStore: pinnedItemStore) + viewModel = SiteStatsInsightsViewModel( + insightsToShow: insightsToShow, + insightsDelegate: self, + viewsAndVisitorsDelegate: self, + insightsStore: insightsStore, + pinnedItemStore: pinnedItemStore + ) } func addViewModelListeners() { @@ -135,11 +140,12 @@ private extension SiteStatsInsightsTableViewController { return } - insightsChangeReceipt = viewModel?.onChange { [weak self] in - self?.refreshGrowAudienceCardIfNecessary() - self?.displayEmptyViewIfNecessary() - self?.refreshTableView() - } + insightsChangeReceipt = viewModel? + .onChange { [weak self] in + self?.refreshGrowAudienceCardIfNecessary() + self?.displayEmptyViewIfNecessary() + self?.refreshTableView() + } } func removeViewModelListeners() { @@ -147,25 +153,27 @@ private extension SiteStatsInsightsTableViewController { } func tableRowTypes() -> [ImmuTableRow.Type] { - return [ViewsVisitorsRow.self, - GrowAudienceRow.self, - CustomizeInsightsRow.self, - LatestPostSummaryRow.self, - TwoColumnStatsRow.self, - PostingActivityRow.self, - TabbedTotalsStatsRow.self, - TopTotalsInsightStatsRow.self, - MostPopularTimeInsightStatsRow.self, - TotalInsightStatsRow.self, - AddInsightRow.self, - TableFooterRow.self, - StatsErrorRow.self, - StatsGhostGrowAudienceImmutableRow.self, - StatsGhostChartImmutableRow.self, - StatsGhostTwoColumnImmutableRow.self, - StatsGhostTopImmutableRow.self, - StatsGhostTabbedImmutableRow.self, - StatsGhostPostingActivitiesImmutableRow.self] + [ + ViewsVisitorsRow.self, + GrowAudienceRow.self, + CustomizeInsightsRow.self, + LatestPostSummaryRow.self, + TwoColumnStatsRow.self, + PostingActivityRow.self, + TabbedTotalsStatsRow.self, + TopTotalsInsightStatsRow.self, + MostPopularTimeInsightStatsRow.self, + TotalInsightStatsRow.self, + AddInsightRow.self, + TableFooterRow.self, + StatsErrorRow.self, + StatsGhostGrowAudienceImmutableRow.self, + StatsGhostChartImmutableRow.self, + StatsGhostTwoColumnImmutableRow.self, + StatsGhostTopImmutableRow.self, + StatsGhostTabbedImmutableRow.self, + StatsGhostPostingActivitiesImmutableRow.self + ] } // MARK: - Table Refreshing @@ -263,9 +271,10 @@ private extension SiteStatsInsightsTableViewController { func refreshGrowAudienceCardIfNecessary() { guard let count = insightsStore.getAllTimeStats()?.viewsCount, - count != self.currentViewCount else { - return - } + count != self.currentViewCount + else { + return + } currentViewCount = count loadPinnedCards() @@ -315,8 +324,9 @@ private extension SiteStatsInsightsTableViewController { let minIndex = isShowingPinnedCard ? 1 : 0 guard let currentIndex = indexOfInsight(insight), - (currentIndex - 1) >= minIndex else { - return false + (currentIndex - 1) >= minIndex + else { + return false } return true @@ -324,15 +334,16 @@ private extension SiteStatsInsightsTableViewController { func canMoveInsightDown(_ insight: InsightType) -> Bool { guard let currentIndex = indexOfInsight(insight), - (currentIndex + 1) < insightsToShow.endIndex else { - return false + (currentIndex + 1) < insightsToShow.endIndex + else { + return false } return true } func indexOfInsight(_ insight: InsightType) -> Int? { - return insightsToShow.firstIndex(of: insight) + insightsToShow.firstIndex(of: insight) } enum ManageInsightConstants { @@ -356,7 +367,10 @@ private extension SiteStatsInsightsTableViewController { extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func displayWebViewWithURL(_ url: URL) { - let webViewController = WebViewControllerFactory.controllerAuthenticatedWithDefaultAccount(url: url, source: "site_stats_insights") + let webViewController = WebViewControllerFactory.controllerAuthenticatedWithDefaultAccount( + url: url, + source: "site_stats_insights" + ) let navController = UINavigationController(rootViewController: webViewController) present(navController, animated: true) } @@ -368,7 +382,9 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { } func showShareForPost(postID: NSNumber, fromView: UIView) { - guard let blogId = SiteStatsInformation.sharedInstance.siteID, let blog = Blog.lookup(withID: blogId, in: mainContext) else { + guard let blogId = SiteStatsInformation.sharedInstance.siteID, + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } @@ -396,7 +412,7 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func showPostingActivityDetails() { let yearData = insightsStore.getYearlyPostingActivity(from: Date()) let postingActivityViewController = PostingActivityViewController.loadFromStoryboard { coder in - return PostingActivityViewController(coder: coder, yearData: yearData) + PostingActivityViewController(coder: coder, yearData: yearData) } navigationController?.pushViewController(postingActivityViewController, animated: true) } @@ -422,8 +438,12 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { // When displaying Annual details, start from the most recent year available. var selectedDate: Date? if statSection == .insightsAnnualSiteStats, - let year = viewModel?.annualInsightsYear() { - var dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: StatsDataHelper.currentDateForSite()) + let year = viewModel?.annualInsightsYear() + { + var dateComponents = Calendar.current.dateComponents( + [.year, .month, .day], + from: StatsDataHelper.currentDateForSite() + ) dateComponents.year = Int(year) selectedDate = Calendar.current.date(from: dateComponents) } @@ -440,7 +460,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func segueToInsightsDetails(statSection: StatSection, selectedDate: Date?, selectedPeriod: StatsPeriodUnit?) { let detailTableViewController = SiteStatsInsightsDetailsTableViewController() - detailTableViewController.configure(statSection: statSection, selectedDate: selectedDate, selectedPeriod: selectedPeriod) + detailTableViewController.configure( + statSection: statSection, + selectedDate: selectedDate, + selectedPeriod: selectedPeriod + ) navigationController?.pushViewController(detailTableViewController, animated: true) } @@ -453,9 +477,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func showPostStats(postID: Int, postTitle: String?, postURL: URL?) { removeViewModelListeners() - let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog(postID: postID, - postTitle: postTitle, - postURL: postURL) + let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog( + postID: postID, + postTitle: postTitle, + postURL: postURL + ) navigationController?.pushViewController(postStatsTableViewController, animated: true) } @@ -475,7 +501,8 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func growAudienceEnablePostSharingButtonTapped() { guard let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) else { + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } @@ -494,15 +521,18 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func growAudienceBloggingRemindersButtonTapped() { guard let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) else { + let blog = Blog.lookup(withID: blogId, in: mainContext) + else { DDLogInfo("Failed to get blog with id \(String(describing: SiteStatsInformation.sharedInstance.siteID))") return } - BloggingRemindersFlow.present(from: self, - for: blog, - source: .statsInsights, - delegate: self) + BloggingRemindersFlow.present( + from: self, + for: blog, + source: .statsInsights, + delegate: self + ) applyTableUpdates() @@ -526,13 +556,22 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { self.navigationController?.popToRootViewController(animated: false) RootViewCoordinator.sharedPresenter.showReader(path: .discover) - Notice(title: NSLocalizedString("Comment to start making connections.", comment: "Hint for users to grow their audience by commenting on other blogs.")).post() + Notice( + title: NSLocalizedString( + "Comment to start making connections.", + comment: "Hint for users to grow their audience by commenting on other blogs." + ) + ) + .post() } let nc = UINavigationController(rootViewController: vc) nc.modalPresentationStyle = .formSheet present(nc, animated: true) { [weak self] in - let text = NSLocalizedString("Follow topics you're interested in and we'll find some blogs you might like.", comment: "Guide for users to follow topics.") + let text = NSLocalizedString( + "Follow topics you're interested in and we'll find some blogs you might like.", + comment: "Guide for users to follow topics." + ) self?.displayNotice(title: text) } @@ -545,8 +584,9 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { func addInsightSelected(_ insight: StatSection) { guard let insightType = insight.insightType, - !insightsToShow.contains(insightType) else { - return + !insightsToShow.contains(insightType) + else { + return } WPAnalytics.track(.statsItemSelectedAddInsight, withProperties: ["insight": insight.title]) @@ -573,7 +613,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { // newly added card will be penultimate row, above the 'Add Stats Card' row let newCardRow = max(self.tableView.numberOfRows(inSection: lastSection) - 2, 0) - self.tableView.scrollToRow(at: IndexPath(row: newCardRow, section: lastSection), at: .middle, animated: true) + self.tableView.scrollToRow( + at: IndexPath(row: newCardRow, section: lastSection), + at: .middle, + animated: true + ) } } @@ -586,9 +630,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { WPAnalytics.track(.statsItemTappedManageInsight) - let alert = UIAlertController(title: insight.title, - message: nil, - preferredStyle: .actionSheet) + let alert = UIAlertController( + title: insight.title, + message: nil, + preferredStyle: .actionSheet + ) if canMoveInsightUp(insightType) { alert.addDefaultActionWithTitle(ManageInsightConstants.moveUp) { [weak self] _ in @@ -645,7 +691,8 @@ extension SiteStatsInsightsTableViewController: StatsInsightsViewsAndVisitorsDel extension SiteStatsInsightsTableViewController: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { guard let navigationController = presentationController.presentedViewController as? UINavigationController, - let controller = navigationController.topViewController as? InsightsManagementViewController else { + let controller = navigationController.topViewController as? InsightsManagementViewController + else { return } @@ -695,15 +742,18 @@ extension SiteStatsInsightsTableViewController: NoResultsViewHost { return } - configureAndDisplayNoResults(on: tableView, - title: NoResultConstants.errorTitle, - subtitle: NoResultConstants.errorSubtitle, - buttonTitle: NoResultConstants.refreshButtonTitle, customizationBlock: { [weak self] noResults in - noResults.delegate = self - if !noResults.isReachable { - noResults.resetButtonText() - } - }) + configureAndDisplayNoResults( + on: tableView, + title: NoResultConstants.errorTitle, + subtitle: NoResultConstants.errorSubtitle, + buttonTitle: NoResultConstants.refreshButtonTitle, + customizationBlock: { [weak self] noResults in + noResults.delegate = self + if !noResults.isReachable { + noResults.resetButtonText() + } + } + ) } private func displayEmptyViewIfNecessary() { @@ -714,22 +764,42 @@ extension SiteStatsInsightsTableViewController: NoResultsViewHost { } displayingEmptyView = true - configureAndDisplayNoResults(on: tableView, - title: NoResultConstants.noInsightsTitle, - subtitle: NoResultConstants.noInsightsSubtitle, - buttonTitle: NoResultConstants.manageInsightsButtonTitle, - image: "wp-illustration-stats-outline") { [weak self] noResults in - noResults.delegate = self + configureAndDisplayNoResults( + on: tableView, + title: NoResultConstants.noInsightsTitle, + subtitle: NoResultConstants.noInsightsSubtitle, + buttonTitle: NoResultConstants.manageInsightsButtonTitle, + image: "wp-illustration-stats-outline" + ) { [weak self] noResults in + noResults.delegate = self } } private enum NoResultConstants { - static let errorTitle = NSLocalizedString("Stats not loaded", comment: "The loading view title displayed when an error occurred") - static let errorSubtitle = NSLocalizedString("There was a problem loading your data, refresh your page to try again.", comment: "The loading view subtitle displayed when an error occurred") - static let refreshButtonTitle = NSLocalizedString("Refresh", comment: "The loading view button title displayed when an error occurred") - static let noInsightsTitle = NSLocalizedString("No insights added yet", comment: "Title displayed when the user has removed all Insights from display.") - static let noInsightsSubtitle = NSLocalizedString("Only see the most relevant stats. Add insights to fit your needs.", comment: "Subtitle displayed when the user has removed all Insights from display.") - static let manageInsightsButtonTitle = NSLocalizedString("Add stats card", comment: "Button title displayed when the user has removed all Insights from display.") + static let errorTitle = NSLocalizedString( + "Stats not loaded", + comment: "The loading view title displayed when an error occurred" + ) + static let errorSubtitle = NSLocalizedString( + "There was a problem loading your data, refresh your page to try again.", + comment: "The loading view subtitle displayed when an error occurred" + ) + static let refreshButtonTitle = NSLocalizedString( + "Refresh", + comment: "The loading view button title displayed when an error occurred" + ) + static let noInsightsTitle = NSLocalizedString( + "No insights added yet", + comment: "Title displayed when the user has removed all Insights from display." + ) + static let noInsightsSubtitle = NSLocalizedString( + "Only see the most relevant stats. Add insights to fit your needs.", + comment: "Subtitle displayed when the user has removed all Insights from display." + ) + static let manageInsightsButtonTitle = NSLocalizedString( + "Add stats card", + comment: "Button title displayed when the user has removed all Insights from display." + ) } } @@ -739,7 +809,8 @@ private extension SiteStatsInsightsTableViewController { func trackNudgeEvent(_ event: WPAnalyticsEvent) { if let blogId = SiteStatsInformation.sharedInstance.siteID, - let blog = Blog.lookup(withID: blogId, in: mainContext) { + let blog = Blog.lookup(withID: blogId, in: mainContext) + { WPAnalytics.track(event, properties: [:], blog: blog) } else { WPAnalytics.track(event) From 9a4bd3fe5501ddda78505b2c1ccf48b9c9b54140 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 15:07:20 +1200 Subject: [PATCH 4/9] Remove the legacy Publicize sharing management UI The v2 connection management screen replaces SharingViewController everywhere. The stats insights nudge now opens the v2 screen, and the dashboard Jetpack Social card is removed along with its promo views. --- Sources/Keystone/WordPress.h | 1 - .../DashboardJetpackSocialCardCellTests.swift | 215 --------- .../System/WordPress-Bridging-Header.h | 1 - WordPress/Classes/Utility/ObjCBridge.swift | 13 - .../DashboardJetpackSocialCardCell.swift | 405 ---------------- .../DashboardCard+Personalization.swift | 2 +- .../Blog Dashboard/Models/DashboardCard.swift | 5 - .../BlogDetailsViewController+Swift.swift | 4 +- ...logDashboardPersonalizationViewModel.swift | 2 +- .../Blog/Sharing/JetpackModuleHelper.swift | 89 ---- .../Blog/Sharing/KeyringAccountHelper.swift | 98 ---- .../Blog/Sharing/PublicizeServiceCell.swift | 100 ---- .../Blog/Sharing/PublicizeServicesState.swift | 29 -- .../SharingAccountViewController.swift | 284 ----------- .../Blog/Sharing/SharingAuthorizationHelper.h | 71 --- .../Blog/Sharing/SharingAuthorizationHelper.m | 440 ----------------- ...haringAuthorizationWebViewController.swift | 190 -------- .../SharingConnectionsViewController.h | 20 - .../SharingConnectionsViewController.m | 300 ------------ .../Sharing/SharingDetailViewController.h | 22 - .../Sharing/SharingDetailViewController.m | 304 ------------ .../Blog/Sharing/SharingViewController.h | 31 -- .../Blog/Sharing/SharingViewController.m | 454 ------------------ .../Blog/Sharing/SharingViewController.swift | 39 -- .../TwitterDeprecationTableFooterView.swift | 129 ----- .../Blog/Sharing/WPStyleGuide+Sharing.swift | 30 -- .../JetpackSocialNoConnectionView.swift | 138 ------ .../Social/JetpackSocialNoSharesView.swift | 84 ---- ...ackSocialSettingsRemainingSharesView.swift | 68 --- ...lishingSocialAccountsTableFooterView.swift | 115 ----- ...SiteStatsInsightsTableViewController.swift | 16 +- WordPress/WordPress.xcodeproj/project.pbxproj | 1 - 32 files changed, 9 insertions(+), 3691 deletions(-) delete mode 100644 Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift delete mode 100644 WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift delete mode 100644 WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift delete mode 100644 WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift delete mode 100644 WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift diff --git a/Sources/Keystone/WordPress.h b/Sources/Keystone/WordPress.h index cbfa826b92b9..627a0a97fc35 100644 --- a/Sources/Keystone/WordPress.h +++ b/Sources/Keystone/WordPress.h @@ -37,7 +37,6 @@ FOUNDATION_EXPORT const unsigned char WordPressVersionString[]; #import #import #import -#import #import #import #import diff --git a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift b/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift deleted file mode 100644 index 06e22dc5d125..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardJetpackSocialCardCellTests.swift +++ /dev/null @@ -1,215 +0,0 @@ -import WordPressShared -import XCTest -@testable import WordPress -@testable import WordPressData - -class DashboardJetpackSocialCardCellTests: CoreDataTestCase { - - private let featureFlags = FeatureFlagOverrideStore() - - override func setUp() { - super.setUp() - - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: true) - } - - override func tearDown() { - super.tearDown() - - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: RemoteFeatureFlag.jetpackSocialImprovements.defaultValue) - } - - // MARK: - `shouldShowCard` tests - - func testCardDisplays() { - // Given, when - let blog = createTestBlog() - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWhenFeatureDisabled() throws { - // Given, when - let blog = createTestBlog() - - // When - featureFlags.override(RemoteFeatureFlag.jetpackSocialImprovements, withValue: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNotDisplayWhenPublicizeNotSupported() { - // Given, when - let blog = createTestBlog(isPublicizeSupported: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNotDisplayWhenPublicizeServicesDoesNotExist() throws { - // Given, when - let blog = createTestBlog(hasServices: false) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWithPublicizeConnections() throws { - // Given, when - let blog = createTestBlog(hasConnections: true) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDoesNotDisplayWhenViewHidden() throws { - // Given - let blog = createTestBlog() - let dotComID = try XCTUnwrap(blog.dotComID) - // FIXME: Using this production type in the test will mess the global state - let repository = UserPersistentStoreFactory.instance() - let key = DashboardJetpackSocialCardCell.Constants.hideNoConnectionViewKey - - // When - repository.set([dotComID.stringValue: true], forKey: key) - addTeardownBlock { - repository.removeObject(forKey: key) - } - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardDisplaysWhenJetpackSiteRunsOutOfShares() throws { - // Given, when - let blog = createTestBlog(hasConnections: true, publicizeInfoState: .exceedingLimit) - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } - - // MARK: - Card state tests - - func testInitialCardState() { - // Given, when - let subject = DashboardJetpackSocialCardCell() - - // Then - XCTAssertEqual(subject.displayState, .none) - } - - func testCardStateWithNoConnections() { - // Given - let subject = DashboardJetpackSocialCardCell() - let blog = createTestBlog() - - // When - subject.configure(blog: blog, viewController: nil, apiResponse: nil) - - // Then - XCTAssertEqual(subject.displayState, .noConnections) - } - - func testCardStateWhenUpdatedWithUnsupportedBlog() { - // Given - let subject = DashboardJetpackSocialCardCell() - let initialBlog = createTestBlog() - let blog = createTestBlog(isPublicizeSupported: false, hasServices: false, hasConnections: true) - subject.configure(blog: initialBlog, viewController: nil, apiResponse: nil) - - // When - subject.configure(blog: blog, viewController: nil, apiResponse: nil) - - // Then - XCTAssertEqual(subject.displayState, .none) - } - - // MARK: Atomic Site Tests - - // In some cases, atomic sites could sometimes get limited sharing from the API. - // We'll need to ignore any sharing limit information if it's a Simple or Atomic site. - // Refs: p9F6qB-dLk-p2#comment-56603 - func testCardOutOfSharesDoesNotDisplayForAtomicSites() throws { - // Given, when - let blog = createTestBlog(isAtomic: true, hasConnections: true, publicizeInfoState: .exceedingLimit) - - // Then - XCTAssertFalse(shouldShowCard(for: blog)) - } - - func testCardNoConnectionDisplaysForAtomicSites() throws { - // Given, when - let blog = createTestBlog(isAtomic: true) - - // Then - XCTAssertTrue(shouldShowCard(for: blog)) - } -} - -// MARK: - Helpers - -private extension DashboardJetpackSocialCardCellTests { - - func shouldShowCard(for blog: Blog) -> Bool { - return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) - } - - enum PublicizeInfoState { - case none - case belowLimit - case exceedingLimit - } - - func createTestBlog(isPublicizeSupported: Bool = true, - isAtomic: Bool = false, - hasServices: Bool = true, - hasConnections: Bool = false, - publicizeInfoState: PublicizeInfoState = .none) -> Blog { - var builder = BlogBuilder(mainContext) - .withAnAccount() - .with(dotComID: 12345) - .with(capabilities: [.publishPosts]) - .with(atomic: isAtomic) - - if isPublicizeSupported { - builder = builder.with(modules: ["publicize"]) - } - - if hasServices { - _ = createPublicizeService() - } - - if hasConnections { - let connection = PublicizeConnection(context: mainContext) - connection.status = "ok" - builder = builder.with(connections: [connection]) - } - - let blog = builder.build() - - switch publicizeInfoState { - case .belowLimit: - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 25 - blog.publicizeInfo = publicizeInfo - break - case .exceedingLimit: - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 0 - blog.publicizeInfo = publicizeInfo - break - default: - break - } - - return blog - } - - func createPublicizeService() -> PublicizeService { - return PublicizeService(context: mainContext) - } -} diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index c4ef1d2696c9..3a6ec9d5ac93 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -33,7 +33,6 @@ #import "SettingsMultiTextViewController.h" #import "SettingTableViewCell.h" #import "SettingsTextViewController.h" -#import "SharingViewController.h" #import "SiteSettingsViewController.h" #import "StatsViewController.h" #import "SuggestionsTableView.h" diff --git a/WordPress/Classes/Utility/ObjCBridge.swift b/WordPress/Classes/Utility/ObjCBridge.swift index 3768cbbc0c97..096fe2b4ecf7 100644 --- a/WordPress/Classes/Utility/ObjCBridge.swift +++ b/WordPress/Classes/Utility/ObjCBridge.swift @@ -15,19 +15,6 @@ import WordPressData SupportTableViewController().showFromTabBar() } - @objc public class func makeSharingAuthorizationViewController( - publicizer: PublicizeService, - url: URL, - blog: Blog, - delegate: SharingAuthorizationDelegate - ) -> UIViewController { - SharingAuthorizationWebViewController(with: publicizer, url: url, for: blog, delegate: delegate) - } - - @objc public class func makeSharingButtonsViewController(blog: Blog) -> UIViewController { - SharingButtonsViewController(blog: blog) - } - @objc public class func trackBlazeEntryPointDisplayed(source: BlazeSource) { BlazeEventsTracker.trackEntryPointDisplayed(for: source) } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift deleted file mode 100644 index e3ff2ed2478e..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/DashboardJetpackSocialCardCell.swift +++ /dev/null @@ -1,405 +0,0 @@ -import SwiftUI -import AutomatticTracks -import WordPressData -import WordPressShared - -class DashboardJetpackSocialCardCell: DashboardCollectionViewCell { - - // MARK: - Properties - - private let repository: UserPersistentRepository - - private var blog: Blog? - private weak var dashboardViewController: BlogDashboardViewController? - - private(set) var displayState: DisplayState = .none { - didSet { - guard oldValue != displayState else { - return - } - updateUI() - } - } - - private var isNoConnectionViewHidden: Bool { - get { - guard let dotComID = blog?.dotComID?.stringValue, - let isNoConnectionHidden = noConnectionHiddenSites[dotComID] else { - return false - } - return isNoConnectionHidden - } - set { - guard let dotComID = blog?.dotComID?.stringValue else { - return - } - var currentHiddenSites = noConnectionHiddenSites - currentHiddenSites[dotComID] = true - repository.set(currentHiddenSites, forKey: Constants.hideNoConnectionViewKey) - } - } - - private var isNoSharesViewHidden: Bool { - get { - guard let dotComID = blog?.dotComID?.stringValue, - let isNoSharesHidden = noSharesHiddenSites[dotComID] else { - return false - } - return isNoSharesHidden - } - set { - guard let dotComID = blog?.dotComID?.stringValue else { - return - } - var currentHiddenSites = noSharesHiddenSites - currentHiddenSites[dotComID] = true - repository.set(currentHiddenSites, forKey: Constants.hideNoSharesViewKey) - } - } - - private var noConnectionHiddenSites: [String: Bool] { - let dictionary = repository.dictionary(forKey: Constants.hideNoConnectionViewKey) as? [String: Bool] - return dictionary ?? [:] - } - - private var noSharesHiddenSites: [String: Bool] { - let dictionary = repository.dictionary(forKey: Constants.hideNoSharesViewKey) as? [String: Bool] - return dictionary ?? [:] - } - - // MARK: - UI Properties - - private var cardTitle: String { - switch displayState { - case .noConnections: - return Constants.connectTitle - case .noShares: - return Constants.noSharesTitle - default: - return "" - } - } - - private var cardFrameView: BlogDashboardCardFrameView { - let frameView = BlogDashboardCardFrameView() - frameView.translatesAutoresizingMaskIntoConstraints = false - frameView.setTitle(cardTitle) - frameView.onEllipsisButtonTap = { } - frameView.ellipsisButton.showsMenuAsPrimaryAction = true - frameView.ellipsisButton.menu = contextMenu - - return frameView - } - - private var contextMenu: UIMenu { - let hideThisAction = UIAction(title: Constants.hideThis, - image: Constants.hideThisImage, - attributes: [UIMenuElement.Attributes.destructive], - handler: contextMenuHandler) - return UIMenu(title: String(), options: .displayInline, children: [hideThisAction]) - } - - var contextMenuHandler: UIActionHandler { - return { [weak self] _ in - guard let self else { - return - } - self.hideCard(for: self.displayState) - } - } - - // MARK: - Initializers - - override init(frame: CGRect) { - self.repository = UserPersistentStoreFactory.instance() - super.init(frame: frame) - observeManagedObjectsChange() - } - - init(repository: UserPersistentRepository = UserPersistentStoreFactory.instance()) { - self.repository = repository - super.init(frame: .zero) - observeManagedObjectsChange() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - DashboardCollectionViewCell - - func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { - self.blog = blog - self.dashboardViewController = viewController - updateDisplayState(for: blog) - } - - // MARK: - Functions - - static func shouldShowCard(for blog: Blog) -> Bool { - guard RemoteFeatureFlag.jetpackSocialImprovements.enabled() else { - return false - } - return showNoConnectionView(for: blog) || showNoSharesView(for: blog) - } - - // MARK: - Constants - - struct Constants { - static let hideNoConnectionViewKey = "dashboard-social-no-connection-view-hidden" - static let hideNoSharesViewKey = "dashboard-social-no-shares-view-hidden" - static let connectTitle = NSLocalizedString("dashboard.card.social.noconnections.title", - value: "Share across your social networks", - comment: "Title for the Jetpack Social dashboard card when the user has no social connections.") - static let noSharesTitle = NSLocalizedString("dashboard.card.social.noshares.title", - value: "You’re out of shares!", - comment: "Title for the Jetpack Social dashboard card when the user has no social shares left.") - static let hideThis = NSLocalizedString("dashboard.card.social.menu.hide", - value: "Hide this", - comment: "Title for a menu action in the context menu on the Jetpack Social dashboard card.") - static let hideThisImage = UIImage(systemName: "minus.circle") - static let cardInsets = EdgeInsets(top: 8.0, leading: 16.0, bottom: 8.0, trailing: 16.0) - static let trackingSource = "home_dashboard" - } - - enum DisplayState { - case none - case noConnections - case noShares - } -} - -// MARK: - Private Functions - -private extension DashboardJetpackSocialCardCell { - - static func showNoConnectionView(for blog: Blog) -> Bool { - guard let context = blog.managedObjectContext, - let dotComID = blog.dotComID?.stringValue, - let services = try? PublicizeService.allSupportedServices(in: context), - let connections = blog.connections else { - return false - } - let repository = UserPersistentStoreFactory.instance() - let hideNoConnectionViewKey = DashboardJetpackSocialCardCell.Constants.hideNoConnectionViewKey - let hiddenSites = (repository.dictionary(forKey: hideNoConnectionViewKey) as? [String: Bool]) ?? [:] - let isNoConnectionViewHidden = hiddenSites[dotComID] ?? false - - return blog.supports(.publicize) - && !services.isEmpty - && connections.isEmpty - && !isNoConnectionViewHidden - } - - func updateDisplayState(for blog: Blog) { - if DashboardJetpackSocialCardCell.showNoConnectionView(for: blog) { - displayState = .noConnections - } else if DashboardJetpackSocialCardCell.showNoSharesView(for: blog) { - displayState = .noShares - } else { - displayState = .none - } - } - - func updateUI() { - guard Thread.isMainThread else { - DispatchQueue.main.async { [weak self] in - self?.updateUI() - } - return - } - - var card: UIView? - switch displayState { - case .noConnections: - card = createNoConnectionCard() - case .noShares: - card = createNoSharesCard() - default: - card = nil - } - - for subview in contentView.subviews { - subview.removeFromSuperview() - } - if let card { - contentView.addSubview(card) - contentView.pinSubviewToAllEdges(card) - contentView.layoutIfNeeded() - } - } - - func createNoConnectionCard() -> UIView? { - guard let context = blog?.managedObjectContext, - let services = try? PublicizeService.allSupportedServices(in: context) else { - // Note: The context and publicize services are checked prior to this call in - // `showNoConnectionView`. This scenario *shouldn't* be possible. - assertionFailure("No managed object context or publicize services") - let error = JetpackSocialError.noConnectionViewInvalidState - CrashLogging.main.logError(error, userInfo: ["source": Constants.trackingSource]) - return nil - } - WPAnalytics.track(.jetpackSocialNoConnectionCardDisplayed, - properties: ["source": Constants.trackingSource]) - let card = cardFrameView - let viewModel = JetpackSocialNoConnectionViewModel(services: services, - padding: Constants.cardInsets, - hideNotNow: true, - bodyTextColor: .secondaryLabel, - onConnectTap: onConnectTap()) - let noConnectionViewController = JetpackSocialNoConnectionView.createHostController(with: viewModel) - card.add(subview: noConnectionViewController.view) - return card - } - - func onConnectTap() -> () -> Void { - return { [weak self] in - WPAnalytics.track(.jetpackSocialNoConnectionCTATapped, - properties: ["source": Constants.trackingSource]) - guard let self, - let blog = self.blog, - let controller = SharingViewController(blog: blog, delegate: self) else { - return - } - self.dashboardViewController?.navigationController?.pushViewController(controller, animated: true) - } - } - - static func showNoSharesView(for blog: Blog) -> Bool { - guard let sharingLimit = blog.sharingLimit, - let dotComID = blog.dotComID?.stringValue, - let connections = blog.connections else { - return false - } - let repository = UserPersistentStoreFactory.instance() - let hideNoSharesViewKey = DashboardJetpackSocialCardCell.Constants.hideNoSharesViewKey - let hiddenSites = (repository.dictionary(forKey: hideNoSharesViewKey) as? [String: Bool]) ?? [:] - let isNoSharesViewHidden = hiddenSites[dotComID] ?? false - - return blog.supports(.publicize) - && connections.contains { !$0.requiresUserAction() } - && !isNoSharesViewHidden - && sharingLimit.remaining == 0 - } - - func createNoSharesCard() -> UIView? { - guard let connections = blog?.connections as? Set else { - assertionFailure("No social connections") - let error = JetpackSocialError.noSharesViewInvalidState - CrashLogging.main.logError(error, userInfo: ["source": "social_dashboard_card"]) - return nil - } - WPAnalytics.track(.jetpackSocialShareLimitDisplayed, - properties: ["source": Constants.trackingSource]) - let card = cardFrameView - let filteredConnections = connections.filter { !$0.requiresUserAction() } - let services = filteredConnections.reduce(into: [PublicizeService.ServiceName]()) { partialResult, connection in - guard let service = PublicizeService.ServiceName(rawValue: connection.service) else { - return - } - if !partialResult.contains(service) { - partialResult.append(service) - } - } - let viewModel = JetpackSocialNoSharesViewModel(services: services, - totalServiceCount: filteredConnections.count, - onSubscribeTap: onSubscribeTap()) - let noSharesView = UIView.embedSwiftUIView(JetpackSocialNoSharesView(viewModel: viewModel)) - card.add(subview: noSharesView) - return card - } - - func onSubscribeTap() -> () -> Void { - return { [weak self] in - WPAnalytics.track(.jetpackSocialUpgradeLinkTapped, - properties: ["source": Constants.trackingSource]) - guard let blog = self?.blog, - let hostname = blog.hostname, - let url = URL(string: "https://wordpress.com/checkout/\(hostname)/jetpack_social_basic_yearly") else { - return - } - let webViewController = WebViewControllerFactory.controller(url: url, - blog: blog, - source: "dashboard_card_no_shares_subscribe_now") { - self?.checkoutDismissed() - } - let navigationController = UINavigationController(rootViewController: webViewController) - self?.dashboardViewController?.present(navigationController, animated: true) - } - } - - func checkoutDismissed() { - guard let blog else { - return - } - let coreDataStack = ContextManager.shared - let service = BlogService(coreDataStack: coreDataStack) - service.syncBlog(blog) { [weak self] in - let sharingLimit: PublicizeInfo.SharingLimit? = coreDataStack.performQuery { context in - guard let dotComID = blog.dotComID, - let blog = Blog.lookup(withID: dotComID, in: context) else { - return nil - } - return blog.sharingLimit - } - if sharingLimit == nil || sharingLimit?.remaining ?? 0 > 0 { - self?.dashboardViewController?.reloadCardsLocally() - } - } failure: { error in - DDLogError("Failed to sync blog after dismissing checkout webview due to error: \(error)") - } - } - - @objc func handleNotification() { - guard let blog else { - return - } - updateDisplayState(for: blog) - } - - func hideCard(for state: DisplayState) { - switch state { - case .noConnections: - isNoConnectionViewHidden = true - WPAnalytics.track(.jetpackSocialNoConnectionCardDismissed, - properties: ["source": Constants.trackingSource]) - case .noShares: - isNoSharesViewHidden = true - WPAnalytics.track(.jetpackSocialShareLimitDismissed, - properties: ["source": Constants.trackingSource]) - default: - break - } - dashboardViewController?.reloadCardsLocally() - } - - func observeManagedObjectsChange() { - NotificationCenter.default.addObserver( - self, - selector: #selector(handleObjectsChange), - name: .NSManagedObjectContextObjectsDidChange, - object: ContextManager.shared.mainContext - ) - } - - @objc func handleObjectsChange(_ notification: Foundation.Notification) { - guard let blog else { - return - } - let updated = notification.userInfo?[NSUpdatedObjectsKey] as? Set ?? Set() - let refreshed = notification.userInfo?[NSRefreshedObjectsKey] as? Set ?? Set() - - if updated.contains(blog) || refreshed.contains(blog) { - updateDisplayState(for: blog) - } - } -} - -// MARK: - SharingViewControllerDelegate - -extension DashboardJetpackSocialCardCell: SharingViewControllerDelegate { - - func didChangePublicizeServices() { - dashboardViewController?.reloadCardsLocally() - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift index b9e1e93f9903..d400a2540d01 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift @@ -27,7 +27,7 @@ extension DashboardCard: BlogDashboardPersonalizable { return "activity-log-card-enabled-site-settings" case .pages: return "pages-card-enabled-site-settings" - case .dynamic, .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty, + case .dynamic, .jetpackBadge, .jetpackInstall, .failure, .ghost, .personalize, .empty, .extensiveLogging: return nil } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift index 7d2c725e4eb3..b875b6cdee86 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift @@ -18,7 +18,6 @@ enum DashboardCard: String, CaseIterable, Sendable { case freeToPaidPlansDashboardCard case domainRegistration case todaysStats = "todays_stats" - case jetpackSocial case draftPosts case scheduledPosts case pages @@ -68,8 +67,6 @@ enum DashboardCard: String, CaseIterable, Sendable { return DashboardPagesListCardCell.self case .activityLog: return DashboardActivityLogCardCell.self - case .jetpackSocial: - return DashboardJetpackSocialCardCell.self case .googleDomains: return DashboardGoogleDomainsCardCell.self } @@ -137,8 +134,6 @@ enum DashboardCard: String, CaseIterable, Sendable { case .activityLog: return DashboardActivityLogCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) - case .jetpackSocial: - return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) case .googleDomains: return FeatureFlag.googleDomainsCard.enabled && isJetpack case .dynamic: diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift index 35449d705936..b47948b869e3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/BlogDetailsViewController+Swift.swift @@ -354,7 +354,9 @@ extension BlogDetailsViewController { } else if let manage = ManageConnectionsHostingController.make(for: blog) { sharingVC = manage } else { - sharingVC = SharingViewController(blog: blog, delegate: nil) + // supports(.publicize) implies a linked WP.com account, so the + // service should always resolve; guards a broken auth state. + return wpAssertionFailure("social connections service unavailable") } trackEvent(.openedSharingManagement, from: source) diff --git a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift index 3bb1714b0e49..488ee565deb6 100644 --- a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift @@ -115,7 +115,7 @@ private extension DashboardCard { case .dynamic, .ghost, .failure, .personalize, .jetpackBadge, .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, - .domainRegistration, .jetpackSocial, + .domainRegistration, .googleDomains, .extensiveLogging: assertionFailure("\(self) card should not appear in the personalization menus") return "" // These cards don't appear in the personalization menus diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift deleted file mode 100644 index e3bfe45ad078..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/JetpackModuleHelper.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import UIKit -import WordPressData -import WordPressUI - -public typealias JetpackModuleHelperViewController = JetpackModuleHelperDelegate & UIViewController - -/// Shows a NoResultsViewController on a given VC and handle enabling -/// a Jetpack module -@objc public class JetpackModuleHelper: NSObject { - private weak var viewController: JetpackModuleHelperViewController? - - private let moduleName: String - private let blog: Blog - private let service: BlogJetpackSettingsService - private let blogService: BlogService - - private var noResultsViewController: NoResultsViewController? - - /// Creates a Jetpack Module Wall that gives the user the option to enable a module - /// - Parameters: - /// - viewController: a UIViewController that conforms to JetpackModuleHelperDelegate - /// - moduleName: a `String` representing the name of the module - /// - blog: a `Blog` - @objc public init(viewController: JetpackModuleHelperViewController, moduleName: String, blog: Blog) { - self.viewController = viewController - self.moduleName = moduleName - self.blog = blog - self.service = BlogJetpackSettingsService(coreDataStack: ContextManager.shared) - self.blogService = BlogService(coreDataStack: ContextManager.shared) - } - - /// Show the No Results View Controller - /// - Parameters: - /// - title: A `String` to display on the title of the NVC - /// - subtitle: A `String` to display as the subtitle of the NVC - @objc public func show(title: String, subtitle: String) { - noResultsViewController = NoResultsViewController.controller() - noResultsViewController?.configure( - title: title, - attributedTitle: nil, - noConnectionTitle: nil, - buttonTitle: NSLocalizedString("Enable", comment: "Title of button to enable publicize."), - subtitle: subtitle, - noConnectionSubtitle: nil, - attributedSubtitle: nil, - attributedSubtitleConfiguration: nil, - image: "mysites-nosites", - subtitleImage: nil, - accessoryView: nil - ) - - if let noResultsViewController, let viewController { - noResultsViewController.delegate = self - - viewController.addChild(noResultsViewController) - viewController.view.addSubview(withFadeAnimation: noResultsViewController.view) - noResultsViewController.didMove(toParent: viewController) - noResultsViewController.view.frame = viewController.view.bounds - } - } -} - -extension JetpackModuleHelper: NoResultsViewControllerDelegate { - public func actionButtonPressed() { - service.updateJetpackModuleActiveSettingForBlog(blog, - module: moduleName, - active: true, - success: { [weak self] in - guard let self else { - return - } - - self.blogService.syncBlogAndAllMetadata(self.blog) { - self.noResultsViewController?.removeFromView() - self.viewController?.jetpackModuleEnabled() - } - }, - failure: { [weak self] _ in - self?.viewController?.displayNotice(title: Constants.error) - }) - } -} - -private extension JetpackModuleHelper { - struct Constants { - static let error = NSLocalizedString("The module couldn't be activated.", comment: "Error shown when a module can not be enabled") - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift deleted file mode 100644 index a5bb8cb3bdda..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/KeyringAccountHelper.swift +++ /dev/null @@ -1,98 +0,0 @@ -import Foundation -import WordPressData -import WordPressKit -import WordPressShared - -/// KeyringAccount is used to normalize the list of avaiable accounts while -/// preserving the owning keyring connection. -/// -struct KeyringAccount { - var name: String // The account name - var externalID: String? // The actual externalID value that should be passed when creating/updating a publicize connection. - var externalIDForConnection: String // The effective external ID that should be used for comparing a keyring account with a PublicizeConnection. - var keyringConnection: KeyringConnection -} - -@objc public class KeyringAccountHelper: NSObject { - - @objc public class ValidationError: NSObject { - @objc public let header: String - @objc public let body: String - @objc public let continueTitle: String - @objc public let cancelTitle: String - - @objc public let continueURL: URL? - - init(header: String, body: String, continueTitle: String, cancelTitle: String, continueURL: URL? = nil) { - self.header = header - self.body = body - self.continueTitle = continueTitle - self.cancelTitle = cancelTitle - self.continueURL = continueURL - } - } - - /// Validate which account type has an error and return the information for an alert - /// - /// - Parameters: - /// - connections: An array of `KeyringConnection` instances to normalize. - /// - publicizeService: The publicize service for the fetched keyring connections. - /// - /// - Returns: An instance of `ValidationError` object, this is a plain object just with the information for an alert. - /// - @objc public func validateConnections(_ connections: [KeyringConnection], with publicizeService: PublicizeService) -> ValidationError? { - let accounts = accountsFromKeyringConnections(connections, with: publicizeService) - - if publicizeService.serviceID == PublicizeService.facebookServiceID, accounts.isEmpty { - return facebookPublicizeAlertFields() - } - - return nil - } - - /// Normalizes available accounts for a KeyringConnection and its `additionalExternalUsers` - /// - /// - Parameter connections: An array of `KeyringConnection` instances to normalize. - /// - /// - Returns: An array of `KeyringAccount` objects. - /// - func accountsFromKeyringConnections(_ connections: [KeyringConnection], with publicizeService: PublicizeService) -> [KeyringAccount] { - var accounts = [KeyringAccount]() - - for connection in connections { - let acct = KeyringAccount(name: connection.externalDisplay, externalID: nil, externalIDForConnection: connection.externalID, keyringConnection: connection) - - // Do not include the service if it only supports external users. - if !publicizeService.externalUsersOnly { - accounts.append(acct) - } - - for externalUser in connection.additionalExternalUsers { - let acct = KeyringAccount(name: externalUser.externalName, externalID: externalUser.externalID, externalIDForConnection: externalUser.externalID, keyringConnection: connection) - accounts.append(acct) - } - } - - return accounts - } -} - -// MARK: - Alert Message Types - -private extension KeyringAccountHelper { - - func facebookPublicizeAlertFields() -> ValidationError { - let alertHeaderMessage = NSLocalizedString("Not Connected", - comment: "Error title for alert, shown to a user who is trying to share to Facebook but does not have any available Facebook Pages.") - let alertBodyMessage = NSLocalizedString("The Facebook connection cannot find any Pages. Publicize cannot connect to Facebook Profiles, only published Pages.", - comment: "Error message shown to a user who is trying to share to Facebook but does not have any available Facebook Pages.") - let continueActionTitle = NSLocalizedString("Learn more", comment: "A button title.") - let cancelActionTitle = SharedStrings.Button.ok - - return ValidationError(header: alertHeaderMessage, - body: alertBodyMessage, - continueTitle: continueActionTitle, - cancelTitle: cancelActionTitle, - continueURL: URL(string: "https://en.support.wordpress.com/publicize/#facebook-pages")) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift deleted file mode 100644 index 591f1faecbcd..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServiceCell.swift +++ /dev/null @@ -1,100 +0,0 @@ -import UIKit -import WordPressData -import WordPressUI -import AsyncImageKit - -public final class PublicizeServiceCell: UITableViewCell { - let iconView = AsyncImageView() - let titleLabel = UILabel() - let detailsLabel = UILabel() - - @objc public class var cellId: String { "PublicizeServiceCell" } - - public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - detailsLabel.font = .preferredFont(forTextStyle: .footnote) - detailsLabel.textColor = .secondaryLabel - - let stackView = UIStackView(alignment: .center, spacing: 12, [ - iconView, - UIStackView(axis: .vertical, alignment: .leading, spacing: 2, [titleLabel, detailsLabel]) - ]) - contentView.addSubview(stackView) - stackView.pinEdges(to: contentView.layoutMarginsGuide) - - NSLayoutConstraint.activate([ - iconView.widthAnchor.constraint(equalToConstant: 28), - iconView.heightAnchor.constraint(equalToConstant: 28), - ]) - iconView.layer.cornerRadius = 8 - iconView.layer.masksToBounds = true - iconView.backgroundColor = UIColor.white - - iconView.contentMode = .scaleAspectFit - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func prepareForReuse() { - super.prepareForReuse() - - accessoryView = .none - iconView.prepareForReuse() - } - - @objc public func configure(with service: PublicizeService, connections: [PublicizeConnection]) { - let name = service.name - if name != .unknown && !name.hasModernRemoteLogo { - iconView.image = name.localIconImage - } else if let imageURL = URL(string: service.icon) { - iconView.setImage(with: imageURL) - } else { - iconView.image = UIImage(named: "social-default") - } - - titleLabel.text = service.label - - detailsLabel.isHidden = connections.isEmpty - if connections.count > 2 { - detailsLabel.text = String(format: Strings.numberOfAccounts, connections.count) - } else { - detailsLabel.text = connections - .map(\.externalDisplay) - .joined(separator: ", ") - } - - if service.isSupported { - if connections.contains(where: { $0.requiresUserAction() }) { - accessoryView = WPStyleGuide.sharingCellWarningAccessoryImageView() - } - } else { - accessoryView = WPStyleGuide.sharingCellErrorAccessoryImageView() - } - } -} - -private extension PublicizeService.ServiceName { - /// We no longer need to provide local overrides for these on this screen - /// as the remote images are good. - var hasModernRemoteLogo: Bool { - [ - PublicizeService.ServiceName.instagram, - PublicizeService.ServiceName.mastodon - ].contains(self) - } -} - -private enum Strings { - static let numberOfAccounts = NSLocalizedString("socialSharing.connectionDetails.nAccount", value: "%d accounts", comment: "The number of connected accounts on a third party sharing service connected to the user's blog. The '%d' is a placeholder for the number of accounts.") -} - -extension PublicizeService.ServiceName { - - /// Returns the local image for the icon representing the social network. - var localIconImage: UIImage { - WPStyleGuide.socialIcon(for: rawValue as NSString) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift deleted file mode 100644 index 888b4d027651..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/PublicizeServicesState.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressData - -@objc public final class PublicizeServicesState: NSObject { - private var connections = Set() -} - -// MARK: - Public Methods -@objc public extension PublicizeServicesState { - func addInitialConnections(_ connections: [PublicizeConnection]) { - connections.forEach { self.connections.insert($0) } - } - - func hasAddedNewConnectionTo(_ connections: [PublicizeConnection]) -> Bool { - guard !connections.isEmpty else { - return false - } - - if connections.count > self.connections.count { - return true - } - - for connection in connections where !self.connections.contains(connection) { - return true - } - - return false - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift deleted file mode 100644 index d8d733c4a367..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAccountViewController.swift +++ /dev/null @@ -1,284 +0,0 @@ -import UIKit -import Gridicons -import WordPressData -import WordPressKit -import WordPressShared -import WordPressUI - -/// Displays a list of available keyring connection accounts that can be used to -/// forge a publicize connection. -/// -@objc open class SharingAccountViewController: UITableViewController { - @objc var publicizeService: PublicizeService - @objc var keyringConnections: [KeyringConnection] - @objc var existingPublicizeConnections: [PublicizeConnection]? - @objc var immutableHandler: ImmuTableViewHandler! - @objc public weak var delegate: SharingAccountSelectionDelegate? - private let keyringAccountHelper = KeyringAccountHelper() - - fileprivate lazy var noResultsViewController: NoResultsViewController = { - let controller = NoResultsViewController.controller() - controller.view.frame = view.frame - addChild(controller) - view.addSubview(controller.view) - controller.didMove(toParent: self) - return controller - }() - - // MARK: - Lifecycle Methods - - @objc public init( - service: PublicizeService, - connections: [KeyringConnection], - existingConnections: [PublicizeConnection]? - ) { - publicizeService = service - keyringConnections = connections - existingPublicizeConnections = existingConnections - - super.init(style: .grouped) - - navigationItem.title = publicizeService.label - } - - required public init?(coder aDecoder: NSCoder) { - // TODO: - fatalError("init(coder:) has not been implemented") - } - - open override func viewDidLoad() { - super.viewDidLoad() - - configureNavbar() - configureTableView() - } - - // MARK: - Configuration - - /// Configures the appearance of the nav bar. - /// - fileprivate func configureNavbar() { - let image = UIImage.gridicon(.cross) - let closeButton = UIBarButtonItem( - image: image, - style: .plain, - target: self, - action: #selector(SharingAccountViewController.handleCloseTapped(_:)) - ) - closeButton.tintColor = UIAppColor.appBarTint - navigationItem.leftBarButtonItem = closeButton - } - - /// Configures the `UITableView` - /// - fileprivate func configureTableView() { - WPStyleGuide.configureColors(view: view, tableView: tableView) - ImmuTable.registerRows([TextRow.self], tableView: tableView) - - immutableHandler = ImmuTableViewHandler(takeOver: self) - immutableHandler.viewModel = tableViewModel() - } - - fileprivate func showNoResultsViewController() { - let title = NSLocalizedString( - "No Accounts Found", - comment: "Title of an error message. There were no third-party service accounts found to setup sharing." - ) - let message = NSLocalizedString( - "Sorry. The social service did not tell us which account could be used for sharing.", - comment: - "An error message shown if a third-party social service does not specify any accounts that an be used with publicize sharing." - ) - noResultsViewController.configure(title: title, subtitle: message) - } - - // MARK: - View Model Wrangling - - /// Builds and returns the ImmuTable view model. - /// - /// - Returns: An ImmuTable instance. - /// - fileprivate func tableViewModel() -> ImmuTable { - var sections = [ImmuTableSection]() - var connectedAccounts = [KeyringAccount]() - var accounts = keyringAccountHelper.accountsFromKeyringConnections(keyringConnections, with: publicizeService) - - if accounts.isEmpty { - showNoResultsViewController() - return ImmuTable(sections: []) - } - - // Filter out connected accounts into a different Array - for (idx, acct) in accounts.enumerated() { - if accountIsConnected(acct) { - connectedAccounts.append(acct) - accounts.remove(at: idx) - break - } - } - - // Build the section for unconnected accounts - var rows = rowsForUnconnectedKeyringAccounts(accounts) - if let section = sectionForUnconnectedKeyringAccountRows(rows) { - sections.append(section) - } - - // Build the section for connected accounts - rows = rowsForConnectedKeyringAccounts(connectedAccounts) - if !rows.isEmpty { - let title = NSLocalizedString( - "Connected", - comment: "Adjective. The title of a list of third-part sharing service account names." - ) - let section = ImmuTableSection(headerText: title, rows: rows, footerText: nil) - sections.append(section) - } - - return ImmuTable(sections: sections) - } - - /// Builds the ImmuTableSection that displays unconnected keyring accounts. - /// - /// - Parameter rows: An array of ImmuTableRow objects appearing in the section. - /// - /// - Returns: An ImmuTableSection or `nil` if there were no rows. - /// - fileprivate func sectionForUnconnectedKeyringAccountRows(_ rows: [ImmuTableRow]) -> ImmuTableSection? { - if rows.isEmpty { - return nil - } - - var title = NSLocalizedString( - "Connecting %@", - comment: - "Connecting is a verb. Title of Publicize account selection. The %@ is a placeholder for the service's name" - ) - title = NSString(format: title as NSString, publicizeService.label) as String - - let manyAccountFooter = NSLocalizedString( - "Select the account you would like to authorize. Note that your posts will be automatically shared to the selected account.", - comment: "Instructional text about the Sharing feature." - ) - let oneAccountFooter = NSLocalizedString( - "Confirm this is the account you would like to authorize. Note that your posts will be automatically shared to this account.", - comment: "Instructional text about the Sharing feature." - ) - let footer = rows.count > 1 ? manyAccountFooter : oneAccountFooter - - return ImmuTableSection(headerText: title, rows: rows, footerText: footer) - } - - /// Builds the ImmuTableSection that displays connected keyring accounts. - /// - /// - Parameter rows: An array of ImmuTableRow objects appearing in the section. - /// - /// - Returns: An ImmuTableSection or `nil` if there were no rows. - /// - fileprivate func rowsForUnconnectedKeyringAccounts(_ accounts: [KeyringAccount]) -> [ImmuTableRow] { - var rows = [ImmuTableRow]() - for acct in accounts { - let row = KeyringRow(title: acct.name, value: "", action: actionForRow(acct)) - - rows.append(row) - } - - return rows - } - - /// Builds an ImmuTableAction that should be performed when a specific row is selected. - /// - /// - Parameter keyringAccount: The keyring account for the row. - /// - /// - Returns: An ImmuTableAction instance. - /// - fileprivate func actionForRow(_ keyringAccount: KeyringAccount) -> ImmuTableAction { - { [unowned self] _ in - self.tableView.deselectSelectedRowWithAnimation(true) - - self.delegate? - .sharingAccountViewController( - self, - selectedKeyringConnection: keyringAccount.keyringConnection, - externalID: keyringAccount.externalID - ) - } - } - - /// Builds ImmuTableRows for the specified keyring accounts. - /// - /// - Parameter accounts: An array of KeyringAccount objects. - /// - /// - Returns: An array of ImmuTableRows representing the keyring accounts. - /// - fileprivate func rowsForConnectedKeyringAccounts(_ accounts: [KeyringAccount]) -> [ImmuTableRow] { - var rows = [ImmuTableRow]() - for acct in accounts { - let row = TextRow(title: acct.name, value: "") - rows.append(row) - } - - return rows - } - - /// Checks if the specified keyring account is connected. - /// - /// - Parameter keyringAccount: The keyring account to check. - /// - /// - Returns: true if the keyring account is being used by an existing publicize connection. False otherwise. - /// - fileprivate func accountIsConnected(_ keyringAccount: KeyringAccount) -> Bool { - guard let existingConnections = existingPublicizeConnections else { - return false - } - - let keyringConnection = keyringAccount.keyringConnection - for existingConnection in existingConnections { - if existingConnection.keyringConnectionID == keyringConnection.keyringID - && existingConnection.keyringConnectionUserID == keyringConnection.userID - && existingConnection.externalID == keyringAccount.externalIDForConnection - { - return true - } - } - - return false - } - - // MARK: - Actions - - /// Notifies the delegate that the user has clicked the close button to dismiss the controller. - /// - /// - Parameter sender: The close button that was tapped. - /// - @objc func handleCloseTapped(_ sender: UIBarButtonItem) { - delegate?.didDismissSharingAccountViewController(self) - } - - /// An ImmuTableRow class. - /// - struct KeyringRow: ImmuTableRow { - static let cell = ImmuTableCell.class(WPTableViewCellValue1.self) - - let title: String - let value: String - let action: ImmuTableAction? - - func configureCell(_ cell: UITableViewCell) { - cell.textLabel?.text = title - cell.detailTextLabel?.text = value - - WPStyleGuide.configureTableViewCell(cell) - } - } -} - -/// Delegate protocol. -/// -@objc public protocol SharingAccountSelectionDelegate: NSObjectProtocol { - func didDismissSharingAccountViewController(_ controller: SharingAccountViewController) - func sharingAccountViewController( - _ controller: SharingAccountViewController, - selectedKeyringConnection keyringConnection: KeyringConnection, - externalID: String? - ) -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h deleted file mode 100644 index b184f86f0838..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.h +++ /dev/null @@ -1,71 +0,0 @@ -@import UIKit; - -@protocol SharingAuthorizationHelperDelegate; -@class PublicizeService; -@class KeyringConnection; -@class PublicizeConnection; -@class Blog; -@class ValidationError; - -/** - A helper class for managing aspects of a publicize service. Supports creating, - updating and deleting connections to a publicize service for a specified blog. - */ -@interface SharingAuthorizationHelper : NSObject - -@property (nonatomic, weak) iddelegate; -@property (nonatomic, strong) UIView *popoverSourceView; - -/** - Returns a new instance. - - @param viewController: A view controller to act as the presenter for the modal auth and account selection view controllers. - @param blog: The blog whose publicize connections are being managed. - @param publicizeServices: The list of available publicize services. - - @return A new instance - */ -- (instancetype)initWithViewController:(UIViewController *)viewController blog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService; - -/** - Starts the process of connecting to publicize service passed when the instance was created. - A `SharingAuthorizationWebViewController` is presented modally allowing the user - to establish the necessary oAuth handshake with a publicize service. - */ -- (void)connectPublicizeService; - -/** - Starts the process of reconnecting a broken publicize connection. - A `SharingAuthorizationWebViewController` is presented modally allowing the user - to repair the necessary oAuth handshake with a publicize service. - - @param publicizeConnection: An existing publicize connection to the publicize service - being managed. - */ -- (void)reconnectPublicizeConnection:(PublicizeConnection *)publicizeConnection; - -@end - -@protocol SharingAuthorizationHelperDelegate - -@optional - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationSucceededForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationFailedForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper authorizationCancelledForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willFetchKeyringsForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didFetchKeyringsForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper keyringFetchFailedForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willConnectToService:(PublicizeService *)service usingKeyringConnection:(KeyringConnection *)keyringConnection; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionFailedForService:(PublicizeService *)service; -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionCancelledForService:(PublicizeService *)service; - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper - requestToShowValidationError:(ValidationError *)validationError - fromViewController:(UIViewController *)viewController; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m deleted file mode 100644 index 9c1228efd5e6..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationHelper.m +++ /dev/null @@ -1,440 +0,0 @@ -@import WordPressShared; - -#import "SharingAuthorizationHelper.h" -#import "BlogService.h" - -#import "WordPress-Swift.h" -@import WordPressData; - - - -@interface SharingAuthorizationHelper() -@property (nonatomic, strong) Blog *blog; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, weak) UIViewController *viewController; -@property (nonatomic, strong) UINavigationController *navController; -@property (nonatomic) BOOL reconnecting; -@end - -@implementation SharingAuthorizationHelper - -#pragma mark - Lifecycle Methods - -- (instancetype)initWithViewController:(UIViewController *)viewController blog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService -{ - self = [self init]; - if (self) { - _blog = blog; - _publicizeService = publicizeService; - _viewController = viewController; - } - return self; -} - - -#pragma mark - Instance Methods - -- (NSArray *)connectionsForService -{ - // TODO: Make a blog extension to get connections for a specific publicize service. - // Use it where ever this method was duplicated. - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:self.publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -/** - Dismisses the currently presented modal view controller. - */ -- (void)dismissNavViewController -{ - [self.navController dismissViewControllerAnimated:YES completion:nil]; -} - -/** - Dismisses the modal and informs the user that a reconnect attempt was successful. - */ -- (void)handleReconnectSucceeded -{ - [self dismissNavViewController]; - NSString *message = NSLocalizedString(@"%@ was reconnected.", @"Let's the user know that a third party sharing service was reconnected. The %@ is a placeholder for the service name."); - message = [NSString stringWithFormat:message, self.publicizeService.label]; - [SVProgressHUD showDismissibleSuccessWithStatus:message]; -} - - -#pragma mark - Authorization Methods - -- (void)connectPublicizeService -{ - self.reconnecting = NO; - [self authorizeWithConnectionURL:[NSURL URLWithString:self.publicizeService.connectURL]]; -} - -- (void)reconnectPublicizeConnection:(PublicizeConnection *)publicizeConnection -{ - self.reconnecting = YES; - [self authorizeWithConnectionURL:[NSURL URLWithString:publicizeConnection.refreshURL]]; -} - -/** - A helper method for presenting an instance of the `SharingAuthorizationWebViewController` - - @param connectionURL: The URL to pass to the SharingAuthorizationWebViewController's constructor. - It should be the REST API URL to either connect or refresh a publicize service connection. - */ -- (void)authorizeWithConnectionURL:(NSURL *)connectionURL -{ - UIViewController *webViewController = [ObjCBridge makeSharingAuthorizationViewControllerWithPublicizer:self.publicizeService url:connectionURL blog:self.blog delegate:self]; - - self.navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; - self.navController.modalPresentationStyle = UIModalPresentationFormSheet; - [self.viewController presentViewController:self.navController animated:YES completion:nil]; -} - - -#pragma mark - Authorization Delegate Methods - -- (void)authorizeDidSucceed:(PublicizeService *)publicizer -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationSucceededForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationSucceededForService:self.publicizeService]; - } - - if (self.reconnecting) { - // Resync publicize connections. - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - [sharingService syncPublicizeConnectionsForBlog:self.blog success:^{ - [self handleReconnectSucceeded]; - } failure:^(NSError *error) { - DDLogError([error description]); - // Even if there is an error syncing the reconnect attempt still succeeded. - [self handleReconnectSucceeded]; - }]; - - } else { - [self fetchKeyringConnectionsForService:publicizer]; - } - -} - -/** - Dismisses the modal view controller prompting the user the connection failed. - - @param publicizer: The publicize service that failed to connect. - @param error: An error with details regarding the connection failure. - */ -- (void)authorize:(PublicizeService *)publicizer didFailWithError:(NSError *)error -{ - DDLogError([error description]); - [self dismissNavViewController]; - - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationFailedForService:self.publicizeService]; - return; - } - - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection failed", @"Message to show when Publicize authorization failed")]; -} - -/** - Dismisses the modal view controller prompting the user the connection was canceled. - - @param publicizer: The publicize service that failed to connect. - */ -- (void)authorizeDidCancel:(PublicizeService *)publicizer -{ - [self dismissNavViewController]; - - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:authorizationCancelledForService:)]) { - [self.delegate sharingAuthorizationHelper:self authorizationCancelledForService:self.publicizeService]; - return; - } - - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection canceled", @"Message to show when Publicize authorization is canceled")]; -} - - -#pragma mark - Keyring Account Selection Methods - -/** - Fetches keyring connections for the specified service. Once keyring connections have been - fetched `showAccountSelectorForKeyrings:` is called. - - @param pubServ: The publicize service for the fetched keyring connections. - */ -- (void)fetchKeyringConnectionsForService:(PublicizeService *)pubServ -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willFetchKeyringsForService:)]) { - [self.delegate sharingAuthorizationHelper:self willFetchKeyringsForService:self.publicizeService]; - } - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService fetchKeyringConnectionsForBlog:self.blog success:^(NSArray *keyringConnections) { - if ([weakSelf.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didFetchKeyringsForService:)]) { - [weakSelf.delegate sharingAuthorizationHelper:weakSelf didFetchKeyringsForService:weakSelf.publicizeService]; - } - - // Fiter matches - NSMutableArray *marr = [NSMutableArray array]; - for (KeyringConnection *keyConn in keyringConnections) { - if ([keyConn.service isEqualToString:pubServ.serviceID]) { - [marr addObject:keyConn]; - } - } - - if ([marr count] == 0) { - DDLogDebug(@"No keyring connections matched serviceID: %@, Returned connections: %@", pubServ.serviceID, keyringConnections); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:keyringFetchFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self keyringFetchFailedForService:self.publicizeService]; - return; - } - NSString *status = [NSString stringWithFormat:NSLocalizedString(@"No connections found for %@", @"Message to show when Keyring connection synchronization succeeded but no matching connections were found. %@ is a service name like Facebook or Twitter"), self.publicizeService.label]; - [SVProgressHUD showDismissibleErrorWithStatus:status]; - return; - } - - //Check errors over accounts and show alert if it's needed - KeyringAccountHelper *keyringAccount = [KeyringAccountHelper new]; - ValidationError *validationError = [keyringAccount validateConnections:marr with:weakSelf.publicizeService]; - if (validationError) { - [weakSelf.delegate sharingAuthorizationHelper:weakSelf - requestToShowValidationError:validationError - fromViewController:weakSelf.navController]; - return; - } - - [weakSelf showAccountSelectorForKeyrings:marr]; - } failure:^(NSError * __unused error) { - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:keyringFetchFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self keyringFetchFailedForService:self.publicizeService]; - return; - } - - NSString *status = [NSString stringWithFormat:NSLocalizedString(@"We had trouble loading connections for %@", @"Message to show when Keyring connection synchronization failed. %@ is a service name like Facebook or Twitter"), self.publicizeService.label]; - [SVProgressHUD showDismissibleErrorWithStatus:status]; - }]; -} - -/** - Presents a modal `SharingAccountViewController` to let the user confirm the third party - account to use for the publicize connection. - - @param keyringConnections: An array of `KeyringConnection` instances. - */ -- (void)showAccountSelectorForKeyrings:(NSArray *)keyringConnections -{ - NSParameterAssert([[keyringConnections firstObject] isKindOfClass:[KeyringConnection class]]); - - SharingAccountViewController *controller = [[SharingAccountViewController alloc] initWithService:self.publicizeService - connections:keyringConnections - existingConnections:[self connectionsForService]]; - controller.delegate = self; - - // Set the view controller stack vs push so there is no back button to contend with. - // There should be no reason for the user to click back to the authorization vc. - [self.navController setViewControllers:@[controller] animated:YES]; -} - - -#pragma mark - SharingAccountSelection Methods - -/** - Returns one or more existing `PublicizeConnections` derived from the specified - keyringConnection. - - @param keyringConnections: An array of `KeyringConnection` instances. - */ -- (PublicizeConnection *)publicizeConnectionUsingKeyringConnection:(KeyringConnection *)keyringConnection -{ - for (PublicizeConnection *connection in self.blog.connections) { - // If the publicize connection is using the keyring connection, and the publicize connection's externalID is either the - if ([connection.keyringConnectionID isEqualToNumber:keyringConnection.keyringID]) { - return connection; - } - } - return nil; -} - -/** - Checks if the specified publicize connection is derived form the specified - keyring connection, and uses the supplied external ID. Returns true if there - is a match. - - @param connection: A `PublicizeConnection` instance. - @param keyringConnections: An array of `KeyringConnection` instances. - @param externalID: The external id of keyring connection, or one of its additional external accounts. - */ -- (BOOL)publicizeConnection:(PublicizeConnection *)connection usesKeyringConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID -{ - // if the specified externalUserID matches the connection's externalID, - // or if the externalUserID is nil, and the connection's externalID is equal to the keyring connections externalID - // then the user choose an already connected account. - if ([externalID isEqualToString:connection.externalID] || - (externalID == nil && [connection.externalID isEqualToString:keyringConnection.externalID])) { - return YES; - } - return NO; -} - -/** - Some KeyringConnections that have addtional external accounts. PublicizeConnections - derived from such a keyring connection can be connected to only one of the keyring - connection's accounts at a time (either the main one or one of the additional ones). - Before updating the external ID of such a PublicizeConnection, prompt the user and - inform them the connection to their curret account will be replaced by their selection. - - @param keyringConnection: The keyring connection in question - @param externalID: The external ID on the keyring connection or one of its additional accounts. - @param currentPublicizeConnection: The existing publicize connection derived from the keyring connection. - */ -- (void)confirmNewConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID disconnectsCurrentConnection:(PublicizeConnection *)currentPublicizeConnection -{ - NSString *accountName = keyringConnection.externalDisplay; - if (![keyringConnection.externalID isEqualToString:externalID]) { - for (KeyringConnectionExternalUser *externalUser in keyringConnection.additionalExternalUsers) { - if ([externalUser.externalID isEqualToString:externalID]) { - accountName = externalUser.externalName; - break; - } - } - } - - NSString *title = NSLocalizedString(@"Connecting %@", @"Connecting is a verb. Title of Publicize account selection. The %@ is a placeholder for the service's name."); - NSString *message = NSLocalizedString(@"Connecting %@ will replace the existing connection to %@.", @"Informs the user of consequences of chooseing a new account to connect to publicize. The %@ characters are placeholders for account names."); - NSString *cancel = NSLocalizedString(@"Cancel", @"Verb. Tapping cancels the publicize account selection."); - NSString *connect = NSLocalizedString(@"Connect", @"Verb. Tapping connects an account to Publicize."); - - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:title, self.publicizeService.label] - message:[NSString stringWithFormat:message, accountName, currentPublicizeConnection.externalDisplay] - preferredStyle:UIAlertControllerStyleAlert]; - - [alertController addCancelActionWithTitle:cancel handler:nil]; - - [alertController addDefaultActionWithTitle:connect handler:^(UIAlertAction * __unused action) { - [self updateConnection:currentPublicizeConnection forKeyringConnection:keyringConnection withExternalID:externalID]; - }]; - - [alertController presentFromRootViewController]; -} - -/** - Updates an existing publicize connection to use the specified external ID. - - @param publicizeConnection: The publicize connection to be modified. - @param keyringConnection: The keyring connection from which the publicize connection is derived - @param externalID: The external id of the keyring connection or one of its additional external accounts. - */ -- (void)updateConnection:(PublicizeConnection *)publicizeConnection forKeyringConnection:(KeyringConnection *)keyringConnection withExternalID:(NSString *)externalID -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willConnectToService:usingKeyringConnection:)]) { - [self.delegate sharingAuthorizationHelper:self willConnectToService:self.publicizeService usingKeyringConnection:keyringConnection]; - } - - [self dismissNavViewController]; - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - - [sharingService updateExternalID:externalID forBlog:self.blog forPublicizeConnection:publicizeConnection success:^{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:publicizeConnection]; - } - } failure:^(NSError *error) { - [self connectionFailedWithError:error]; - }]; -} - -/** - Forges a publicize connection between the blog and publicize servcie with which - the `SharingAuthorizationHelper` was initialized. - - @param keyConn: The keyring connection from which to create a publicize connection - @param externalUserID: The external ID of one of the keyring connection's additional external accounts. - Should be nil if not connecting to one of the additional external accounts. - */ -- (void)connectToServiceWithKeyringConnection:(KeyringConnection *)keyConn andExternalUserID:(NSString *)externalUserID -{ - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:willConnectToService:usingKeyringConnection:)]) { - [self.delegate sharingAuthorizationHelper:self willConnectToService:self.publicizeService usingKeyringConnection:keyConn]; - } - - [self dismissNavViewController]; - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService createPublicizeConnectionForBlog:self.blog - keyring:keyConn - externalUserID:externalUserID - success:^(PublicizeConnection *pubConn) { - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:pubConn]; - } - } - failure:^(NSError *error) { - [self connectionFailedWithError:error]; - }]; -} - -/** - A convenience method for handling an error when making a publicize connection. - - @param error: The error that occurred. - */ -- (void)connectionFailedWithError:(NSError *)error -{ - DDLogError([error description]); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:connectionFailedForService:)]) { - [self.delegate sharingAuthorizationHelper:self connectionFailedForService:self.publicizeService]; - return; - } - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection failed", @"Message to show when Publicize connect failed")]; -} - - -#pragma mark - SharingAccount Delegate Methods - -- (void)didDismissSharingAccountViewController:(SharingAccountViewController *)controller -{ - [self dismissNavViewController]; - - NSString *str = [NSString stringWithFormat:@"The %@ connection could not be made because no account was selected.", self.publicizeService.label]; - DDLogDebug(str); - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:connectionCancelledForService:)]) { - [self.delegate sharingAuthorizationHelper:self connectionCancelledForService:self.publicizeService]; - return; - } - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Connection canceled", @"Message to show when Publicize connection is canceled by the user.")]; -} - -- (void)sharingAccountViewController:(SharingAccountViewController *)controller - selectedKeyringConnection:(KeyringConnection *)keyringConnection - externalID:(NSString *)externalID -{ - PublicizeConnection *connection = [self publicizeConnectionUsingKeyringConnection:keyringConnection]; - - if (!connection) { - [self connectToServiceWithKeyringConnection:keyringConnection andExternalUserID:externalID]; - return; - } - - // Check to see if the chosen connection and external ID matches an existin PublicizeConnection. - // If this is true the user has selected the already connected account and we can just treat it as a success. - if ([self publicizeConnection:connection usesKeyringConnection:keyringConnection withExternalID:externalID]) { - // The user selected the existing connection. Treat this as success and bail. - if ([self.delegate respondsToSelector:@selector(sharingAuthorizationHelper:didConnectToService:withPublicizeConnection:)]) { - [self.delegate sharingAuthorizationHelper:self didConnectToService:self.publicizeService withPublicizeConnection:connection]; - } - [self dismissNavViewController]; - return; - } - - // The user has selected a different account on an keyring connection that is already in use. - // We need to ask the user to confirm, because connecting the new account will disconnect the old one. - [self confirmNewConnection:keyringConnection withExternalID:externalID disconnectsCurrentConnection:connection]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift deleted file mode 100644 index c8bdadb62fce..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingAuthorizationWebViewController.swift +++ /dev/null @@ -1,190 +0,0 @@ -import JetpackSocial -import WebKit -import CoreMedia -import WordPressData - -@objc -public protocol SharingAuthorizationDelegate: NSObjectProtocol { - @objc - func authorize(_ publicizer: PublicizeService, didFailWithError error: NSError) - - @objc - func authorizeDidSucceed(_ publicizer: PublicizeService) - - @objc - func authorizeDidCancel(_ publicizer: PublicizeService) -} - -class SharingAuthorizationWebViewController: WebKitViewController { - private static let loginURL = "https://wordpress.com/wp-login.php" - - /// Verification loading -- dismiss on completion - /// - private var loadingVerify: Bool = false - - /// Publicize service being authorized - /// - private let publicizer: PublicizeService - - private var hosts = [String]() - - private weak var delegate: SharingAuthorizationDelegate? - - init(with publicizer: PublicizeService, url: URL, for blog: Blog, delegate: SharingAuthorizationDelegate) { - self.delegate = delegate - self.publicizer = publicizer - - let configuration = WebViewControllerConfiguration(url: url) - configuration.authenticate(blog: blog) - configuration.secureInteraction = true - - super.init(configuration: configuration) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - View Lifecycle - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - cleanupCookies() - } - - // MARK: - Cookies Management - - /// Saves the host from the specidied URL for cleaning up cookies when done. - /// - /// - Parameters: - /// - url: the URL to retrieve the host from. - /// - func saveHostForCookiesCleanup(from url: URL) { - guard let host = url.host, - !host.contains("wordpress"), - !hosts.contains(host) - else { - return - } - - let components = host.components(separatedBy: ".") - - // A bit of paranioa here. The components should never be less than two but just in case... - guard let hostName = components.count > 1 ? components[components.count - 2] : components.first else { - return - } - - hosts.append(hostName) - } - - /// Cleanup cookies - /// - func cleanupCookies() { - let storage = HTTPCookieStorage.shared - - guard let cookies = storage.cookies else { - // Nothing to cleanup - return - } - - for cookie in cookies { - for host in hosts { - if cookie.domain.contains(host) { - storage.deleteCookie(cookie) - } - } - } - } - - // MARK: - Misc - - override func close() { - guard let delegate else { - super.close() - return - } - - delegate.authorizeDidCancel(publicizer) - } - - private func handleAuthorizationAllowed() { - // Note: There are situations where this can be called in error due to how - // individual services choose to reply to an authorization request. - // Delegates should expect to handle a false positive. - delegate?.authorizeDidSucceed(publicizer) - } -} - -// MARK: - WKNavigationDelegate - -extension SharingAuthorizationWebViewController { - override func webView( - _ webView: WKWebView, - decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void - ) { - decidePolicy(webView: webView, navigationAction: navigationAction, decisionHandler: decisionHandler) - } - - private func decidePolicy( - webView: WKWebView, - navigationAction: WKNavigationAction, - decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void - ) { - // Prevent a second verify load by someone happy clicking. - guard !loadingVerify, - let url = navigationAction.request.url - else { - decisionHandler(.cancel) - return - } - - let action = PublicizeConnectionURLMatcher.authorizeAction(for: url) - - switch action { - case .none: - fallthrough - case .unknown: - fallthrough - case .request: - super.webView(webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) - return - case .verify: - loadingVerify = true - decisionHandler(.allow) - return - case .deny: - decisionHandler(.cancel) - close() - return - } - } - - override func webView( - _ webView: WKWebView, - didFailProvisionalNavigation navigation: WKNavigation!, - withError error: Error - ) { - if loadingVerify && (error as NSError).code == NSURLErrorCancelled { - // Authenticating to Facebook and Twitter can return an false - // NSURLErrorCancelled (-999) error. However the connection still succeeds. - handleAuthorizationAllowed() - return - } - - super.webView(webView, didFailProvisionalNavigation: navigation, withError: error) - } - - override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if let url = webView.url { - saveHostForCookiesCleanup(from: url) - } - - if loadingVerify { - handleAuthorizationAllowed() - } else { - super.webView(webView, didFinish: navigation) - } - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h deleted file mode 100644 index ee3365597da0..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.h +++ /dev/null @@ -1,20 +0,0 @@ -#import - -@class Blog; -@class PublicizeService; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingConnectionsViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * - * @return New instance of SharingViewController - */ -- (instancetype)initWithBlog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m deleted file mode 100644 index de4cbc6d5a4f..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingConnectionsViewController.m +++ /dev/null @@ -1,300 +0,0 @@ -#import "SharingConnectionsViewController.h" - -#import "BlogService.h" -#import "SharingDetailViewController.h" -#import "SharingAuthorizationHelper.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressShared; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingConnectionsViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, strong) SharingAuthorizationHelper *helper; -@property (nonatomic, assign) BOOL connecting; - -@end - -@implementation SharingConnectionsViewController - -#pragma mark - Life Cycle Methods - -- (void)dealloc -{ - self.helper.delegate = nil; -} - -- (instancetype)initWithBlog:(Blog *)blog publicizeService:(PublicizeService *)publicizeService -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - self = [self initWithStyle:UITableViewStyleInsetGrouped]; - if (self) { - _blog = blog; - _publicizeService = publicizeService; - _helper = [[SharingAuthorizationHelper alloc] initWithViewController:self blog:blog publicizeService:publicizeService]; - _helper.delegate = self; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = self.publicizeService.label; - - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - self.tableView.cellLayoutMarginsFollowReadableWidth = YES; - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:CellIdentifier]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.tableView reloadData]; -} - - -#pragma mark - Instance Methods - -- (NSArray *)connectionsForService -{ - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:self.publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -- (BOOL)hasConnectedAccounts -{ - return [[self connectionsForService] count] > 0; -} - -- (void)showDetailForConnection:(PublicizeConnection *)connection -{ - SharingDetailViewController *controller = [[SharingDetailViewController alloc] initWithBlog:self.blog - publicizeConnection:connection]; - [self.navigationController pushViewController:controller animated:YES]; -} - - -#pragma mark - TableView Delegate Methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - if ([self.publicizeService.serviceID isEqualToString:PublicizeService.googlePlusServiceID]) { - if ([self hasConnectedAccounts]) { - return 1; - } else { - return 0; - } - } else if ([self hasConnectedAccounts] && self.publicizeService.isSupported) { - return 2; - } - - return 1; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - NSString *title; - if ([self hasConnectedAccounts] && section == 0) { - title = NSLocalizedString(@"Connected Accounts", @"Noun. Title. Title for the list of accounts for third party sharing services."); - } else { - NSString *format = NSLocalizedString(@"Share post to %@", @"Title. `The `%@` is a placeholder for the service name."); - title = [NSString stringWithFormat:format, self.publicizeService.label]; - } - return title; -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - if ([self hasConnectedAccounts] && section == 0) { - return nil; - } - NSString *title = NSLocalizedString(@"Connect to automatically share your blog posts to %@", @"Instructional text appearing below a `Connect` button. The `%@` is a placeholder for the name of a third-party sharing service."); - return [NSString stringWithFormat:title, self.publicizeService.label]; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section -{ - if (self.publicizeService.isSupported) { - return nil; - } - - TwitterDeprecationTableFooterView *footerView = [TwitterDeprecationTableFooterView new]; - footerView.presentingViewController = self; - footerView.source = @"social_connection_detail"; - - return footerView; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - if ([self hasConnectedAccounts] && section == 0) { - return [[self connectionsForService] count]; - } - - return 1; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - WPTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; - // resets the cell - [WPStyleGuide configureTableViewCell:cell]; - cell.accessoryType = UITableViewCellAccessoryNone; - cell.accessoryView = nil; - cell.textLabel.textAlignment = NSTextAlignmentNatural; - - if ([self hasConnectedAccounts] && indexPath.section == 0) { - [self configurePublicizeCell:cell atIndexPath:indexPath]; - - } else { - [self configureConnectionCell:cell atIndexPath:indexPath]; - } - - return cell; -} - -- (void)configureConnectionCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath -{ - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.text = [self titleForConnectionCell]; - if (self.connecting) { - UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; - cell.accessoryView = activityView; - cell.selectionStyle = UITableViewCellSelectionStyleNone; - [activityView startAnimating]; - } else { - cell.accessoryView = nil; - cell.selectionStyle = UITableViewCellSelectionStyleGray; - } -} - -- (NSString *)titleForConnectionCell -{ - if (self.connecting) { - return NSLocalizedString(@"Connecting...", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); - } - if ([self hasConnectedAccounts]) { - return NSLocalizedString(@"Connect Another Account", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); - } - return NSLocalizedString(@"Connect", @"Verb. Text label. Allows the user to connect to a third-party sharing service like Facebook or Twitter."); -} - -- (void)configurePublicizeCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath -{ - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - PublicizeConnection *connection = [[self connectionsForService] objectAtIndex:indexPath.row]; - cell.textLabel.text = connection.externalDisplay; - - if (![self.publicizeService isSupported]) { - cell.accessoryView = [WPStyleGuide sharingCellErrorAccessoryImageView]; - } else if ([connection requiresUserAction]) { - cell.accessoryView = [WPStyleGuide sharingCellWarningAccessoryImageView]; - } -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - - if ([self hasConnectedAccounts] && indexPath.section == 0) { - PublicizeConnection *connection = [[self connectionsForService] objectAtIndex:indexPath.row]; - [self showDetailForConnection:connection]; - return; - } - - if (self.connecting) { - return; - } - - [self handleConnectTapped:indexPath]; -} - - -#pragma mark - Actions - -- (void)handleConnectTapped:(NSIndexPath *)indexPath -{ - if ([UIDevice isPad]) { - UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; - self.helper.popoverSourceView = cell.textLabel; - } - - [self.helper connectPublicizeService]; -} - -- (void)handleContinueURLTapped:(NSURL*)url -{ - UIApplication *application = [UIApplication sharedApplication]; - - if ([application canOpenURL:url]) { - [application openURL:url options:@{} completionHandler:nil]; - } -} - - -#pragma mark - SharingAuthorizationHelper Delegate Methods - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper connectionFailedForService:(PublicizeService *)service -{ - self.connecting = NO; - [self.tableView reloadData]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper willConnectToService:(PublicizeService *)service usingKeyringConnection:(KeyringConnection *)keyringConnection -{ - self.connecting = YES; - [self.tableView reloadData]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection -{ - self.connecting = NO; - [self.tableView reloadData]; - [self showDetailForConnection:keyringConnection]; -} - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper - requestToShowValidationError:(ValidationError *)validationError - fromViewController:(UIViewController *)viewController -{ - [self dismissViewControllerAnimated:YES completion:nil]; - - UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:validationError.header - message:validationError.body - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:validationError.cancelTitle - style:UIAlertActionStyleCancel - handler:nil]; - __weak SharingConnectionsViewController *sharingConnectionsVC = self; - UIAlertAction* continueAction = [UIAlertAction actionWithTitle:validationError.continueTitle - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * __unused action) { - if (validationError.continueURL) { - [sharingConnectionsVC handleContinueURLTapped: validationError.continueURL]; - } - }]; - - [alertVC addAction:continueAction]; - [alertVC addAction:cancelAction]; - [self presentViewController:alertVC animated:YES completion:nil]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h deleted file mode 100644 index 5c2f48450e3d..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.h +++ /dev/null @@ -1,22 +0,0 @@ -#import - -@class Blog; -@class PublicizeConnection; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingDetailViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * @param connection the relevant publicize connection - * - * @return New instance of SharingDetailViewController - */ -- (instancetype)initWithBlog:(Blog *)blog - publicizeConnection:(PublicizeConnection *)connection; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m deleted file mode 100644 index 9a1027836e59..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingDetailViewController.m +++ /dev/null @@ -1,304 +0,0 @@ -#import "SharingDetailViewController.h" -#import "BlogService.h" -#import "SharingAuthorizationHelper.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressShared; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingDetailViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) PublicizeConnection *publicizeConnection; -@property (nonatomic, strong) PublicizeService *publicizeService; -@property (nonatomic, strong) SharingAuthorizationHelper *helper; -@end - -@implementation SharingDetailViewController - -- (void)dealloc -{ - self.helper.delegate = nil; -} - -- (instancetype)initWithBlog:(Blog *)blog - publicizeConnection:(PublicizeConnection *)connection -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - NSParameterAssert([connection isKindOfClass:[PublicizeConnection class]]); - self = [super initWithStyle:UITableViewStyleGrouped]; - if (self) { - _blog = blog; - _publicizeConnection = connection; - PublicizeService *publicizeService = [PublicizeService lookupPublicizeServiceNamed:connection.service inContext:[self managedObjectContext]]; - if (publicizeService) { - self.helper = [[SharingAuthorizationHelper alloc] initWithViewController:self - blog:self.blog - publicizeService:publicizeService]; - self.helper.delegate = self; - self.publicizeService = publicizeService; - } - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = self.publicizeConnection.externalDisplay; - - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - [self.tableView registerClass:[WPTableViewCell class] forCellReuseIdentifier:CellIdentifier]; -} - - -#pragma mark - Instance Methods - -- (void)openFacebookFAQ -{ - NSURL *url = [NSURL URLWithString:@"https://en.blog.wordpress.com/2018/07/23/sharing-options-from-wordpress-com-to-facebook-are-changing/"]; - [[UIApplication sharedApplication] openURL:url options:[NSDictionary new] completionHandler:nil]; -} - -- (NSString *)textForFacebookFooter -{ - NSString *title = NSLocalizedString(@"As of August 1, 2018, Facebook no longer allows direct sharing of posts to Facebook Profiles. Connections to Facebook Pages remain unchanged.", @"Message shown to users who have an old publicize connection to a facebook profile."); - return [NSString stringWithFormat:title, self.publicizeConnection.label]; -} - -- (NSString *)textForBrokenConnectionFooter -{ - NSString *title = NSLocalizedString(@"There is an issue connecting to %@. Reconnect to continue publicizing.", @"Informs the user about an issue connecting to the third-party sharing service. The `%@` is a placeholder for the service name."); - return [NSString stringWithFormat:title, self.publicizeConnection.label]; -} - -- (void)configureReconnectCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Reconnect", @"Verb. Text label. Tapping attempts to reconnect a third-party sharing service to the user's blog."); - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [UIColor murielPrimary]; -} - -- (void)configureLearnMoreCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Learn More", @"Title of a button. Tapping allows the user to learn more about the specific error."); - [WPStyleGuide configureTableViewActionCell:cell]; - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [UIColor murielPrimary]; -} - -- (void)configureDisconnectCell: (UITableViewCell *)cell -{ - cell.textLabel.text = NSLocalizedString(@"Disconnect", @"Verb. Text label. Tapping disconnects a third-party sharing service from the user's blog."); - [WPStyleGuide configureTableViewDestructiveActionCell:cell]; -} - -- (NSManagedObjectContext *)managedObjectContext -{ - return self.blog.managedObjectContext; -} - -/// Returns true if the service is supported by Jetpack Social, but the connection is broken. -- (BOOL)isSupportedConnectionBroken -{ - return [self.publicizeConnection isBroken] && self.publicizeService.isSupported; -} - - -#pragma mark - TableView Delegate Methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - if ([self.publicizeConnection requiresUserAction] && self.publicizeService.isSupported) { - return 3; - } - - return 2; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - if (section == 0) { - return NSLocalizedString(@"Settings", @"Section title"); - } - - return nil; -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - if (section == 0) { - return NSLocalizedString(@"Allow this connection to be used by all admins and users of your site.", @""); - } - - if (section == 1) { - if ([self.publicizeConnection mustDisconnectFacebook]) { - return [self textForFacebookFooter]; - } - - if ([self isSupportedConnectionBroken]) { - return [self textForBrokenConnectionFooter]; - } - } - - return nil; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - return 1; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (indexPath.section == 0) { - cell = [self switchTableViewCell]; - - } else if (indexPath.section == 1 && [self isSupportedConnectionBroken]) { - [self configureReconnectCell:cell]; - - } else if (indexPath.section == 1 && [self.publicizeConnection mustDisconnectFacebook]) { - [self configureLearnMoreCell:cell]; - - } else { - [self configureDisconnectCell:cell]; - } - - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - [tableView deselectRowAtIndexPath:indexPath animated:YES]; - - // Do nothing when the service is unsupported. - // The first section's cell has a tap recognizer applied on the entire cell, preventing touch events from - // bubbling up to the table view delegate. But when the cell's interaction is disabled, this method will be called. - if (indexPath.section == 0 && !self.publicizeService.isSupported) { - return; - } - - if (indexPath.section == 1 && [self isSupportedConnectionBroken]) { - [self reconnectPublicizeConnection]; - } else if (indexPath.section == 1 && [self.publicizeConnection mustDisconnectFacebook]) { - [self openFacebookFAQ]; - } else { - [self promptToConfirmDisconnect]; - } -} - -- (SwitchTableViewCell *)switchTableViewCell -{ - SwitchTableViewCell *cell = [[SwitchTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; - cell.textLabel.text = NSLocalizedString(@"Available to all users", @""); - cell.on = self.publicizeConnection.shared; - - // disable interaction if the service is unsupported. - if (!self.publicizeService.isSupported) { - [cell.textLabel setTextColor:[UIColor secondaryLabelColor]]; - cell.userInteractionEnabled = NO; - } - - __weak __typeof(self) weakSelf = self; - cell.onChange = ^(BOOL value) { - [weakSelf updateSharedGlobally:value]; - }; - - return cell; -} - - -#pragma mark - Publicize Connection Methods - -- (void)updateSharedGlobally:(BOOL)shared -{ - __weak __typeof(self) weakSelf = self; - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService updateSharedForBlog:self.blog - shared:shared - forPublicizeConnection:self.publicizeConnection - success:nil - failure:^(NSError *error) { - DDLogError([error description]); - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Change failed", @"Message to show when Publicize globally shared setting failed")]; - [weakSelf.tableView reloadData]; - }]; -} - -- (void)reconnectPublicizeConnection -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - - __weak __typeof(self) weakSelf = self; - if (self.helper == nil) { - [sharingService syncPublicizeServicesForBlog:self.blog - success:^{ - [[weakSelf helper] reconnectPublicizeConnection:weakSelf.publicizeConnection]; - } - failure:^(NSError * _Nullable error) { - [WPError showNetworkingAlertWithError:error]; - }]; - } else { - [self.helper reconnectPublicizeConnection:weakSelf.publicizeConnection]; - } -} - -- (void)disconnectPublicizeConnection -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService deletePublicizeConnectionForBlog:self.blog pubConn:self.publicizeConnection success:nil failure:^(NSError *error) { - DDLogError([error description]); - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Disconnect failed", @"Message to show when Publicize disconnect failed")]; - }]; - - // Since the service optimistically deletes the connection, go ahead and pop. - [self.navigationController popViewControllerAnimated:YES]; -} - -- (void)promptToConfirmDisconnect -{ - NSString *message = NSLocalizedString(@"Disconnecting this account means published posts will no longer be automatically shared to %@", @"Explanatory text for the user. The `%@` is a placeholder for the name of a third-party sharing service."); - message = [NSString stringWithFormat:message, self.publicizeConnection.label]; - UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil - message:message - preferredStyle:UIAlertControllerStyleActionSheet]; - [alert addDestructiveActionWithTitle:NSLocalizedString(@"Disconnect", @"Verb. Title of a button. Tapping disconnects a third-party sharing service from the user's blog.") - handler:^(UIAlertAction * __unused action) { - [self disconnectPublicizeConnection]; - }]; - - [alert addCancelActionWithTitle:NSLocalizedString(@"Cancel", @"Verb. A button title.") handler:nil]; - - if ([UIDevice isPad]) { - alert.modalPresentationStyle = UIModalPresentationPopover; - [self presentViewController:alert animated:YES completion:nil]; - - NSUInteger section = [self isSupportedConnectionBroken] ? 2 : 1; - UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; - UIPopoverPresentationController *presentationController = alert.popoverPresentationController; - presentationController.permittedArrowDirections = UIPopoverArrowDirectionAny; - presentationController.sourceView = cell.textLabel; - presentationController.sourceRect = cell.textLabel.bounds; - } else { - [self presentViewController:alert animated:YES completion:nil]; - } -} - - -#pragma mark - SharingAuthenticationHelper Delegate Methods - -- (void)sharingAuthorizationHelper:(SharingAuthorizationHelper *)helper didConnectToService:(PublicizeService *)service withPublicizeConnection:(PublicizeConnection *)keyringConnection -{ - [SVProgressHUD showDismissibleSuccessWithStatus:NSLocalizedString(@"Reconnected", @"Message shwon to confirm a publicize connection has been successfully reconnected.")]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h deleted file mode 100644 index 6132645f5ff4..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.h +++ /dev/null @@ -1,31 +0,0 @@ -#import - -@protocol SharingViewControllerDelegate - -- (void)didChangePublicizeServices; - -@end - -@protocol JetpackModuleHelperDelegate - -- (void)jetpackModuleEnabled; - -@end - -@class Blog; - -/** - * @brief Controller to display Calypso sharing options - */ -@interface SharingViewController : UITableViewController - -/** - * @brief Convenience initializer - * - * @param blog the blog from where to read the information from - * - * @return New instance of SharingViewController - */ -- (instancetype)initWithBlog:(Blog *)blog delegate:(id)delegate; - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m deleted file mode 100644 index 7ee4c37807f0..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.m +++ /dev/null @@ -1,454 +0,0 @@ -#import "SharingViewController.h" -#import "BlogService.h" -#import "SharingConnectionsViewController.h" -#import "WordPress-Swift.h" -@import WordPressData; -@import WordPressUI; -@import WordPressShared; - -typedef NS_ENUM(NSInteger, SharingSectionType) { - SharingSectionUndefined = 1000, - SharingSectionAvailableServices, - SharingSectionUnsupported, - SharingSectionSharingButtons -}; - -static NSString *const CellIdentifier = @"CellIdentifier"; - -@interface SharingViewController () - -@property (nonatomic, strong, readonly) Blog *blog; -@property (nonatomic, strong) NSArray *publicizeServices; - -// Contains Publicize services that are currently available for use. -@property (nonatomic, strong) NSArray *supportedServices; - -// Contains unsupported Publicize services that are deprecated or temporarily disabled. -@property (nonatomic, strong) NSArray *unsupportedServices; - -// A list of `SharingSectionType` that represents the sections displayed in the table view. -@property (nonatomic, strong) NSArray *sections; - -@property (nonatomic, weak) id delegate; -@property (nonatomic) PublicizeServicesState *publicizeServicesState; -@property (nonatomic) JetpackModuleHelper *jetpackModuleHelper; - -@end - -@implementation SharingViewController - -- (instancetype)initWithBlog:(Blog *)blog delegate:(id)delegate -{ - NSParameterAssert([blog isKindOfClass:[Blog class]]); - self = [self initWithStyle:UITableViewStyleInsetGrouped]; - if (self) { - _blog = blog; - _publicizeServices = [NSMutableArray new]; - _supportedServices = @[]; - _unsupportedServices = @[]; - _delegate = delegate; - _publicizeServicesState = [PublicizeServicesState new]; - } - return self; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.navigationItem.title = NSLocalizedString(@"Sharing", @"Title for blog detail sharing screen."); - - if (self.isModal) { - self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone - target:self - action:@selector(doneButtonTapped)]; - } - - [self.tableView registerClass:[PublicizeServiceCell class] forCellReuseIdentifier:PublicizeServiceCell.cellId]; - self.tableView.cellLayoutMarginsFollowReadableWidth = YES; - [WPStyleGuide configureColorsForView:self.view andTableView:self.tableView]; - [self.publicizeServicesState addInitialConnections:[self allConnections]]; - - self.navigationController.presentationController.delegate = self; - - if ([self.blog supports:BlogFeaturePublicize]) { - [self syncServices]; - } else { - self.jetpackModuleHelper = [[JetpackModuleHelper alloc] initWithViewController:self moduleName:@"publicize" blog:self.blog]; - - [self.jetpackModuleHelper showWithTitle:NSLocalizedString(@"Enable Publicize", "Text shown when the site doesn't have the Publicize module enabled.") subtitle:NSLocalizedString(@"In order to share your published posts to your social media you need to enable the Publicize module.", "Title of button to enable publicize.")]; - - self.tableView.dataSource = NULL; - } -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self.tableView reloadData]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [ReachabilityUtils dismissNoInternetConnectionNotice]; -} - -- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController -{ - [self notifyDelegatePublicizeServicesChangedIfNeeded]; -} - -- (void)refreshPublicizers -{ - self.publicizeServices = [PublicizeService allPublicizeServicesInContext:[self managedObjectContext] error:nil]; - - // Separate supported and unsupported Publicize services. - NSPredicate *supportedPredicate = [NSPredicate predicateWithFormat:@"status == %@", PublicizeService.defaultStatus]; - self.supportedServices = [self.publicizeServices filteredArrayUsingPredicate:supportedPredicate]; - - NSPredicate *unsupportedPredicate = [NSPredicate predicateWithFormat:@"status == %@", PublicizeService.unsupportedStatus]; - NSArray *unsupportedList = [self.publicizeServices filteredArrayUsingPredicate:unsupportedPredicate]; - - // only list unsupported services with existing connections. - NSMutableArray *unsupportedServicesWithConnections = [NSMutableArray new]; - for (PublicizeService *service in unsupportedList) { - if ([self connectionsForService:service].count > 0) { - [unsupportedServicesWithConnections addObject:service]; - } - } - self.unsupportedServices = unsupportedServicesWithConnections; - - // Refresh table sections in case anything changes. - [self refreshSections]; - - [self.tableView reloadData]; -} - -- (void)doneButtonTapped -{ - [self notifyDelegatePublicizeServicesChangedIfNeeded]; - [self dismissViewControllerAnimated:YES completion:nil]; -} - -#pragma mark - Table view sections - -- (void)refreshSections { - NSMutableArray *sections = [NSMutableArray new]; - - if ([self.supportedServices count] > 0) { - [sections addObject:@(SharingSectionAvailableServices)]; - } - - if (self.unsupportedServices.count > 0) { - [sections addObject:@(SharingSectionUnsupported)]; - } - - if ([self.blog supports:BlogFeatureShareButtons]) { - [sections addObject:@(SharingSectionSharingButtons)]; - } - - self.sections = sections; -} - -- (SharingSectionType)sectionTypeForIndex:(NSInteger)index -{ - if (index >= self.sections.count) { - return SharingSectionUndefined; - } - - return [self.sections objectAtIndex:index].intValue; -} - -#pragma mark - UITableView Delegate methods - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return self.sections.count; -} - -- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return NSLocalizedString(@"Jetpack Social Connections", @"Section title for Publicize services in Sharing screen"); - - case SharingSectionUnsupported: - return NSLocalizedStringWithDefaultValue( - @"social.section.disabledTwitter.header", - nil, - [NSBundle mainBundle], - @"Twitter Auto-Sharing Is No Longer Available", - @"Section title for the disabled Twitter service in the Social screen"); - - case SharingSectionSharingButtons: - return NSLocalizedString(@"Sharing Buttons", @"Section title for the sharing buttons section in the Sharing screen"); - - default: - return nil; - } -} - -- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - if (sectionType == SharingSectionAvailableServices) { - return NSLocalizedString(@"Connect your favorite social media services to automatically share new posts with friends.", @""); - } - return nil; -} - -- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section -{ - [WPStyleGuide configureTableViewSectionFooter:view]; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return self.supportedServices.count; - case SharingSectionUnsupported: - return self.unsupportedServices.count; - case SharingSectionSharingButtons: - return 1; - default: - return 0; - } -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - // TODO: Remove? - if ([self.publicizeServices count] > 0) { - PublicizeService *publicizer = self.publicizeServices[indexPath.row]; - NSArray *connections = [self connectionsForService:publicizer]; - if ([publicizer.serviceID isEqualToString:PublicizeService.googlePlusServiceID] && [connections count] == 0) { // Temporarily hiding Google+ - return 0; - } - } - - return UITableViewAutomaticDimension; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: // fallthrough - case SharingSectionUnsupported: - return [self makePublicizeCellAtIndexPath:indexPath]; - case SharingSectionSharingButtons: - return [self makeManageButtonCell]; - default: - return [UITableViewCell new]; - } -} - -- (PublicizeService *)publicizeServiceForIndexPath:(NSIndexPath *)indexPath -{ - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: - return self.supportedServices[indexPath.row]; - case SharingSectionUnsupported: - return self.unsupportedServices[indexPath.row]; - default: - return nil; - } -} - -- (UITableViewCell *)makePublicizeCellAtIndexPath:(NSIndexPath *)indexPath -{ - PublicizeServiceCell *cell = [self.tableView dequeueReusableCellWithIdentifier:PublicizeServiceCell.cellId]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - - PublicizeService *publicizer = [self publicizeServiceForIndexPath:indexPath]; - NSArray *connections = [self connectionsForService:publicizer]; - - // TODO: Remove? - if ([publicizer.serviceID isEqualToString:PublicizeService.googlePlusServiceID] && [connections count] == 0) { // Temporarily hiding Google+ - cell.hidden = YES; - return cell; - } - - [cell configureWith:publicizer connections:connections]; - return cell; -} - -- (UITableViewCell *)makeManageButtonCell -{ - WPTableViewCell *cell = [self. - tableView dequeueReusableCellWithIdentifier:CellIdentifier]; - if (!cell) { - cell = [[WPTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; - } - - [WPStyleGuide configureTableViewCell:cell]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - cell.textLabel.text = NSLocalizedString(@"Manage", @"Verb. Text label. Tapping displays a screen where the user can configure 'share' buttons for third-party services."); - cell.detailTextLabel.text = nil; - cell.imageView.image = nil; - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - UIViewController *controller; - SharingSectionType sectionType = [self sectionTypeForIndex:indexPath.section]; - switch (sectionType) { - case SharingSectionAvailableServices: // fallthrough - case SharingSectionUnsupported: { - PublicizeService *publicizer = [self publicizeServiceForIndexPath:indexPath]; - controller = [[SharingConnectionsViewController alloc] initWithBlog:self.blog publicizeService:publicizer]; - [WPAppAnalytics track:WPAnalyticsStatSharingOpenedPublicize withBlog:self.blog]; - break; - } - - case SharingSectionSharingButtons: - controller = [ObjCBridge makeSharingButtonsViewControllerWithBlog:self.blog]; - [WPAppAnalytics track:WPAnalyticsStatSharingOpenedSharingButtonSettings withBlog:self.blog]; - break; - - default: - return; - } - - [self.navigationController pushViewController:controller animated:YES]; -} - -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section -{ - SharingSectionType sectionType = [self sectionTypeForIndex:section]; - switch (sectionType) { - case SharingSectionUnsupported: - return [self makeTwitterDeprecationFooterView]; - - case SharingSectionSharingButtons: - if ([SharingViewController jetpackBrandingVisibile]) { - return [self makeJetpackBadge]; - } - break; - - default: - break; - } - - return nil; -} - -#pragma mark - JetpackModuleHelper - -- (void)jetpackModuleEnabled -{ - self.tableView.dataSource = self; - [self syncServices]; -} - -#pragma mark - Publicizer management - -// TODO: Good candidate for a helper method -- (NSArray *)connectionsForService:(PublicizeService *)publicizeService -{ - NSMutableArray *connections = [NSMutableArray array]; - for (PublicizeConnection *pubConn in self.blog.connections) { - if ([pubConn.service isEqualToString:publicizeService.serviceID]) { - [connections addObject:pubConn]; - } - } - return [NSArray arrayWithArray:connections]; -} - -- (NSArray *)allConnections -{ - NSMutableArray *allConnections = [NSMutableArray new]; - for (PublicizeService *service in self.publicizeServices) { - NSArray *connections = [self connectionsForService:service]; - if (connections.count > 0) { - [allConnections addObjectsFromArray:connections]; - } - } - return allConnections; -} - --(void)notifyDelegatePublicizeServicesChangedIfNeeded -{ - if ([self.publicizeServicesState hasAddedNewConnectionTo:[self allConnections]]) { - [self.delegate didChangePublicizeServices]; - } -} - -- (NSManagedObjectContext *)managedObjectContext -{ - return self.blog.managedObjectContext; -} - --(void)syncServices -{ - // Optimistically sync the sharing buttons. - [self syncSharingButtonsIfNeeded]; - - // Refreshes the tableview. - [self refreshPublicizers]; - - // Syncs servcies and connections. - [self syncPublicizeServices]; - -} - --(void)showConnectionError -{ - [ReachabilityUtils showNoInternetConnectionNoticeWithMessage: ReachabilityUtils.noConnectionMessage]; -} - -- (void)syncPublicizeServices -{ - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService syncPublicizeServicesForBlog:self.blog success:^{ - [weakSelf syncConnections]; - } failure:^(NSError * __unused error) { - if (!ReachabilityUtils.isInternetReachable) { - [weakSelf showConnectionError]; - } else { - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Jetpack Social service synchronization failed", @"Message to show when Publicize service synchronization failed")]; - [weakSelf refreshPublicizers]; - } - }]; -} - -- (void)syncConnections -{ - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; - __weak __typeof__(self) weakSelf = self; - [sharingService syncPublicizeConnectionsForBlog:self.blog success:^{ - [weakSelf refreshPublicizers]; - } failure:^(NSError * __unused error) { - if (!ReachabilityUtils.isInternetReachable) { - [weakSelf showConnectionError]; - } else { - [SVProgressHUD showDismissibleErrorWithStatus:NSLocalizedString(@"Jetpack Social connection synchronization failed", @"Message to show when Publicize connection synchronization failed")]; - [weakSelf refreshPublicizers]; - } - }]; -} - -- (void)syncSharingButtonsIfNeeded -{ - // Sync sharing buttons if they have never been synced. Otherwise, the - // management vc can worry about fetching the latest sharing buttons. - NSArray *buttons = [SharingButton allSharingButtonsForBlog:self.blog inContext:[self managedObjectContext] error:nil]; - if ([buttons count] > 0) { - return; - } - - SharingService *sharingService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - [sharingService syncSharingButtonsForBlog:self.blog success:nil failure:^(NSError *error) { - DDLogError([error description]); - }]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift deleted file mode 100644 index f4f6c5a29bea..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/SharingViewController.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit - -extension SharingViewController { - - static let jetpackBadgePadding: CGFloat = 30 - - @objc - public static func jetpackBrandingVisibile() -> Bool { - return JetpackBrandingVisibility.all.enabled - } - - @objc - public func makeJetpackBadge() -> UIView { - let textProvider = JetpackBrandingTextProvider(screen: JetpackBadgeScreen.sharing) - let badge = JetpackButton.makeBadgeView(title: textProvider.brandingText(), - topPadding: Self.jetpackBadgePadding, - bottomPadding: Self.jetpackBadgePadding, - target: self, - selector: #selector(presentJetpackOverlay)) - return badge - } - - @objc - public func presentJetpackOverlay() { - JetpackBrandingCoordinator.presentOverlay(from: self) - JetpackBrandingAnalyticsHelper.trackJetpackPoweredBadgeTapped(screen: .sharing) - } - - // MARK: Twitter Deprecation - - @objc - public func makeTwitterDeprecationFooterView() -> TwitterDeprecationTableFooterView { - let footerView = TwitterDeprecationTableFooterView() - footerView.presentingViewController = self - footerView.source = "social_connection_list" - - return footerView - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift deleted file mode 100644 index 2c619b1f54b5..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/TwitterDeprecationTableFooterView.swift +++ /dev/null @@ -1,129 +0,0 @@ -import UIKit -import WordPressUI - -/// A subclass implementation of `UITableViewHeaderFooterView` that displays a text with a tappable link. -/// Specifically used for Twitter deprecation purposes. -/// -@objc public class TwitterDeprecationTableFooterView: UITableViewHeaderFooterView { - - // The view controller that will present the web view. - @objc public weak var presentingViewController: UIViewController? = nil - - // For tracking purposes. See https://wp.me/pctCYC-OI#tracks - @objc public var source: String? = nil - - private let label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.font = .preferredFont(forTextStyle: .footnote) - label.textColor = .secondaryLabel - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.lineHeightMultiple = Constants.lineHeightMultiple - - let attributedString = NSMutableAttributedString(string: "\(Constants.deprecationNoticeText) ", attributes: [ - .paragraphStyle: paragraphStyle - ]) - - if let attachmentURL = Constants.blogPostURL { - let hyperlinkText = NSAttributedString(string: Constants.hyperlinkText, attributes: [ - .paragraphStyle: paragraphStyle, - .attachment: attachmentURL, - .foregroundColor: UIAppColor.primary - ]) - attributedString.append(hyperlinkText) - } - - label.attributedText = attributedString - label.isUserInteractionEnabled = true - - return label - }() - - // MARK: Methods - - public override init(reuseIdentifier: String?) { - super.init(reuseIdentifier: reuseIdentifier) - setupSubviews() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setupSubviews() - } -} - -// MARK: - Private methods - -private extension TwitterDeprecationTableFooterView { - - func setupSubviews() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped)) - label.addGestureRecognizer(tapGesture) - - contentView.addSubview(label) - contentView.pinSubviewToAllEdgeMargins(label) - } - - @objc func labelTapped(_ sender: UITapGestureRecognizer) { - guard let presentingViewController, - let source, - let attributedText = label.attributedText else { - return - } - - // detect the tap location within the attributed text. - let location = sender.location(in: label) - let textStorage = NSTextStorage(attributedString: attributedText) - let textContainer = NSTextContainer(size: label.bounds.size) - let layoutManager = NSLayoutManager() - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - - textContainer.lineFragmentPadding = 0 - textContainer.lineBreakMode = label.lineBreakMode - textContainer.maximumNumberOfLines = label.numberOfLines - - let characterIndex = layoutManager.characterIndex(for: location, - in: textContainer, - fractionOfDistanceBetweenInsertionPoints: nil) - - guard characterIndex < textStorage.length else { - return - } - - let attributes = attributedText.attributes(at: characterIndex, effectiveRange: nil) - guard let attachmentURL = attributes[.attachment] as? URL else { - return - } - - WPAnalytics.track(.jetpackSocialTwitterNoticeLinkTapped, properties: ["source": source]) - - // Ideally this shouldn't be the responsibility of this class, but I'm keeping it simple since it's temporary. - let webViewController = WebViewControllerFactory.controller(url: attachmentURL, source: source) - let navigationController = UINavigationController(rootViewController: webViewController) - presentingViewController.present(navigationController, animated: true) - } - - // MARK: Constants - - enum Constants { - // adjust attributedText's line height. The default settings look slightly stretched. - static let lineHeightMultiple = 0.9 - - static let blogPostURL = URL(string: "https://wordpress.com/blog/2023/04/29/why-twitter-auto-sharing-is-coming-to-an-end/") - - static let deprecationNoticeText = NSLocalizedString( - "social.twitterDeprecation.text", - value: "Twitter auto-sharing is no longer available due to Twitter's changes in terms and pricing.", - comment: "A smallprint that hints the reason behind why Twitter is deprecated." - ) - - static let hyperlinkText = NSLocalizedString( - "social.twitterDeprecation.link", - value: "Find out more", - comment: "Text for a hyperlink that allows the user to learn more about the Twitter deprecation." - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift index 145ff1700ced..64aac9fd82d9 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift @@ -6,36 +6,6 @@ import WordPressShared /// Sharing feature. /// extension WPStyleGuide { - /// Create an UIImageView showing the notice gridicon. - /// - /// - Returns: A UIImageView - /// - @objc public class func sharingCellWarningAccessoryImageView() -> UIImageView { - let imageSize = 20.0 - let horizontalPadding = 8.0 - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize + horizontalPadding, height: imageSize)) - - imageView.image = UIImage(named: "sharing-notice") - imageView.tintColor = jazzyOrange() - imageView.contentMode = .right - return imageView - } - - /// Create an UIImageView showing the notice gridicon. - /// - /// - Returns: A UIImageView - /// - @objc public class func sharingCellErrorAccessoryImageView() -> UIImageView { - let imageSize = 20.0 - let horizontalPadding = 8.0 - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageSize + horizontalPadding, height: imageSize)) - - imageView.image = UIImage(named: "sharing-notice") - imageView.tintColor = .systemRed - imageView.contentMode = .right - return imageView - } - /// Creates an icon for the specified service, or a the default social icon. /// /// - Parameters: diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift deleted file mode 100644 index c338ac4a5abd..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoConnectionView.swift +++ /dev/null @@ -1,138 +0,0 @@ -import SwiftUI -import AsyncImageKit -import WordPressData -import WordPressUI - -struct JetpackSocialNoConnectionView: View { - - let viewModel: JetpackSocialNoConnectionViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(spacing: -5.0) { - ForEach(viewModel.icons, id: \.self) { icon in - iconImage(image: icon.image, url: icon.url) - } - } - .accessibilityElement() - .accessibilityLabel(Constants.iconGroupAccessibilityLabel) - Text(Constants.bodyText) - .font(.callout) - .foregroundColor(Color(viewModel.bodyTextColor)) - HStack { - Text(Constants.connectText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onConnectTap?() - } - if !viewModel.hideNotNow { - Spacer() - Text(Constants.notNowText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onNotNowTap?() - } - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(viewModel.padding) - .background(Color(viewModel.preferredBackgroundColor)) - } - - func iconImage(image: UIImage, url: URL?) -> some View { - CachedAsyncImage(url: url) { image in - image - .icon(backgroundColor: viewModel.preferredBackgroundColor) - } placeholder: { - Image(uiImage: image) - .icon(backgroundColor: viewModel.preferredBackgroundColor) - } - } -} - -// MARK: - JetpackSocialNoConnectionView Extension - -extension JetpackSocialNoConnectionView { - static func createHostController(with viewModel: JetpackSocialNoConnectionViewModel = JetpackSocialNoConnectionViewModel()) -> UIHostingController { - let hostController = UIHostingController(rootView: JetpackSocialNoConnectionView(viewModel: viewModel)) - hostController.view.translatesAutoresizingMaskIntoConstraints = false - hostController.view.backgroundColor = viewModel.preferredBackgroundColor - return hostController - } -} - -// MARK: - Image Extension - -private extension Image { - func icon(backgroundColor: UIColor) -> some View { - self - .resizable() - .frame(width: 32.0, height: 32.0) - .background(Color(backgroundColor)) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(backgroundColor), lineWidth: 2.0)) - } -} - -// MARK: - View model - -struct JetpackSocialNoConnectionViewModel { - - struct IconInfo: Hashable { - let image: UIImage - let url: URL? - } - - let padding: EdgeInsets - let hideNotNow: Bool - let preferredBackgroundColor: UIColor - let bodyTextColor: UIColor - let onConnectTap: (() -> Void)? - let onNotNowTap: (() -> Void)? - let icons: [IconInfo] - - init(services: [PublicizeService] = [], - padding: EdgeInsets = Constants.defaultPadding, - hideNotNow: Bool = false, - preferredBackgroundColor: UIColor? = nil, - bodyTextColor: UIColor = .label, - onConnectTap: (() -> Void)? = nil, - onNotNowTap: (() -> Void)? = nil) { - self.padding = padding - self.hideNotNow = hideNotNow - self.preferredBackgroundColor = preferredBackgroundColor ?? Constants.defaultBackgroundColor - self.bodyTextColor = bodyTextColor - self.onConnectTap = onConnectTap - self.onNotNowTap = onNotNowTap - - var images = [IconInfo]() - for service in services { - let icon = WPStyleGuide.socialIcon(for: service.serviceID as NSString) - let url: URL? = service.name == .unknown ? URL(string: service.icon) : nil - images.append(IconInfo(image: icon, url: url)) - } - self.icons = images - } -} - -// MARK: - Constants - -private struct Constants { - static let defaultPadding = EdgeInsets(top: 16.0, leading: 16.0, bottom: 24.0, trailing: 16.0) - static let defaultBackgroundColor = UIColor.secondarySystemGroupedBackground - static let bodyText = NSLocalizedString("social.noconnection.body", - value: "Increase your traffic by auto-sharing your posts with your friends on social media.", - comment: "Body text for the Jetpack Social no connection view") - static let connectText = NSLocalizedString("social.noconnection.connectAccounts", - value: "Connect accounts", - comment: "Title for the connect button to add social sharing for the Jetpack Social no connection view") - static let notNowText = NSLocalizedString("social.noconnection.notnow", - value: "Not now", - comment: "Title for the not now button to hide the Jetpack Social no connection view") - static let iconGroupAccessibilityLabel = NSLocalizedString("social.noconnection.icons.accessibility.label", - value: "Social media icons", - comment: "Accessibility label for the social media icons in the Jetpack Social no connection view") -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift deleted file mode 100644 index 07aae6fd5481..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialNoSharesView.swift +++ /dev/null @@ -1,84 +0,0 @@ -import SwiftUI -import WordPressData -import WordPressUI - -struct JetpackSocialNoSharesView: View { - - let viewModel: JetpackSocialNoSharesViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 4.0) { - HStack(spacing: -5.0) { - ForEach(viewModel.services, id: \.self) { service in - iconImage(service.localIconImage) - } - } - .padding(.bottom, 8.0) - .accessibilityElement() - .accessibilityLabel(Constants.iconGroupAccessibilityLabel) - Text(bodyText) - .font(.callout) - .foregroundColor(Color(UIColor.secondaryLabel)) - .padding(.bottom, 5.0) - Text(Constants.subscribeText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onSubscribeTap() - } - .accessibilityAddTraits(.isButton) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding(EdgeInsets(top: 0.0, leading: 16.0, bottom: 8.0, trailing: 16.0)) - } - - var bodyText: String { - if viewModel.totalServiceCount > 1 { - return String(format: Constants.pluralShareTextFormat, viewModel.totalServiceCount) - } - return Constants.singularShareText - } - - func iconImage(_ image: UIImage) -> some View { - Image(uiImage: image) - .resizable() - .frame(width: 32.0, height: 32.0) - .opacity(0.36) - .background(Color(UIColor.secondarySystemGroupedBackground)) - .clipShape(Circle()) - .overlay(Circle().stroke(Color(UIColor.secondarySystemGroupedBackground), lineWidth: 2.0)) - } -} - -// MARK: - View model - -struct JetpackSocialNoSharesViewModel { - - let services: [PublicizeService.ServiceName] - let totalServiceCount: Int - let onSubscribeTap: () -> Void - - init(services: [PublicizeService.ServiceName], totalServiceCount: Int, onSubscribeTap: @escaping () -> Void) { - self.services = services - self.totalServiceCount = totalServiceCount - self.onSubscribeTap = onSubscribeTap - } -} - -// MARK: - Constants - -private struct Constants { - - static let pluralShareTextFormat = NSLocalizedString("social.noshares.body.plural", - value: "Your posts won’t be shared to your %1$d social accounts.", - comment: "Plural body text for the Jetpack Social no shares dashboard card. %1$d is the number of social accounts the user has.") - static let singularShareText = NSLocalizedString("social.noshares.body.singular", - value: "Your posts won’t be shared to your social account.", - comment: "Singular body text for the Jetpack Social no shares dashboard card.") - static let subscribeText = NSLocalizedString("social.noshares.subscribe", - value: "Subscribe to share more", - comment: "Title for the button to subscribe to Jetpack Social on the no shares dashboard card") - static let iconGroupAccessibilityLabel = NSLocalizedString("social.noshares.icons.accessibility.label", - value: "Social media icons", - comment: "Accessibility label for the social media icons in the Jetpack Social no shares dashboard card") -} diff --git a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift b/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift deleted file mode 100644 index eb481e6287f5..000000000000 --- a/WordPress/Classes/ViewRelated/Jetpack/Social/JetpackSocialSettingsRemainingSharesView.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI -import WordPressUI - -struct JetpackSocialSettingsRemainingSharesView: View { - - let viewModel: JetpackSocialRemainingSharesViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 4.0) { - HStack(spacing: 8.0) { - if viewModel.displayWarning { - Image("icon-warning") - .resizable() - .frame(width: 16.0, height: 16.0) - } - remainingText - } - Text(Constants.subscribeText) - .font(.callout) - .foregroundColor(Color(UIAppColor.primary)) - .onTapGesture { - viewModel.onSubscribeTap() - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(EdgeInsets(top: 12.0, leading: 16.0, bottom: 12.0, trailing: 16.0)) - .background(Color(UIColor.secondarySystemGroupedBackground)) - } - - private var remainingText: some View { - let sharesRemainingString = String(format: Constants.remainingTextFormat, viewModel.remaining) - let sharesRemaining = Text(sharesRemainingString).font(.callout) - if viewModel.displayWarning { - return sharesRemaining - } - let remainingDays = Text(Constants.remainingEndText).font(.callout).foregroundColor(.secondary) - return sharesRemaining + remainingDays - } - - private struct Constants { - static let remainingTextFormat = NSLocalizedString("postsettings.social.shares.text.format", - value: "%1$d social shares remaining", - comment: "Beginning text of the remaining social shares a user has left." - + " %1$d is their current remaining shares." - + " This text is combined with ' in the next 30 days' if there is no warning displayed.") - static let remainingEndText = NSLocalizedString("postsettings.social.remainingshares.text.part", - value: " in the next 30 days", - comment: "The second half of the remaining social shares a user has." - + " This is only displayed when there is no social limit warning.") - static let subscribeText = NSLocalizedString("postsettings.social.remainingshares.subscribe", - value: "Subscribe now to share more", - comment: "Title for the button to subscribe to Jetpack Social on the remaining shares view") - } -} - -struct JetpackSocialRemainingSharesViewModel { - let remaining: Int - let displayWarning: Bool - let onSubscribeTap: () -> Void - - init(remaining: Int, - displayWarning: Bool, - onSubscribeTap: @escaping () -> Void) { - self.remaining = remaining - self.displayWarning = displayWarning - self.onSubscribeTap = onSubscribeTap - } -} diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift b/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift deleted file mode 100644 index f9b60644b184..000000000000 --- a/WordPress/Classes/ViewRelated/Post/Publishing/Views/PrepublishingSocialAccountsTableFooterView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import UIKit -import SwiftUI -import WordPressUI - -class PrepublishingSocialAccountsTableFooterView: UITableViewHeaderFooterView, Reusable { - - init(remaining: Int, - showsWarning: Bool, - onButtonTap: (() -> Void)?, - reuseIdentifier: String? = PrepublishingSocialAccountsTableFooterView.defaultReuseID) { - super.init(reuseIdentifier: reuseIdentifier) - - let footerView = PrepublishingSocialAccountsFooterView(remaining: remaining, - showsWarning: showsWarning, - onButtonTap: onButtonTap) - let viewToEmbed = UIView.embedSwiftUIView(footerView) - contentView.addSubview(viewToEmbed) - contentView.pinSubviewToAllEdges(viewToEmbed) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -// MARK: - SwiftUI View - -struct PrepublishingSocialAccountsFooterView: View { - - @State var remaining: Int - - @State var showsWarning: Bool - - @ScaledMetric(relativeTo: .callout) private var warningIconLength = 16.0 - - var onButtonTap: (() -> Void)? - - var body: some View { - VStack(spacing: 16.0) { - remainingSharesLabel - subscribeButton - } - .padding(EdgeInsets(top: 16.0, leading: 0, bottom: 0, trailing: 0)) - } - - var remainingSharesLabel: some View { - Label { - Text(sharesText) - .font(.callout) - .foregroundColor(Color(showsWarning ? Constants.warningColor : .label)) - } icon: { - if showsWarning { - Image("icon-warning") - .resizable() - .frame(width: warningIconLength, height: warningIconLength) - } - } - .accessibilityLabel(showsWarning ? "\(Constants.warningIconAccessibilityText), \(sharesText)" : sharesText) - } - - var subscribeButton: some View { - Button { - onButtonTap?() - } label: { - Text(Constants.subscribeButtonText) - .padding(12.0) - .frame(maxWidth: .infinity) // needs to be set here to make the button stretch full-width. - } - .buttonStyle(SubscribeButtonStyle()) - } - - private var sharesText: String { - String(format: Constants.remainingSharesLabelTextFormat, remaining) - } - - private struct SubscribeButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(Constants.buttonLabelFont) - .foregroundColor(.white) - .background(Color(configuration.isPressed ? Constants.buttonHighlightedColor : Constants.buttonColor)) - .clipShape(RoundedRectangle(cornerRadius: 8.0)) - } - } - - private enum Constants { - static let buttonLabelFont = Font.title3.weight(.medium) - static let buttonColor = UIAppColor.primary - static let buttonHighlightedColor = UIAppColor.jetpackGreen(.shade70) - static let warningColor = UIAppColor.warning(.shade50) - - static let remainingSharesLabelTextFormat = NSLocalizedString( - "prepublishing.socialAccounts.footer.remainingShares.text", - value: "%1$d social shares remaining in the next 30 days", - comment: """ - Text shown below the list of social accounts to indicate how many social shares available for the site. - Note that the '30 days' part is intended to be a static value. - %1$d is a placeholder for the amount of remaining shares. - Example: 27 social shares remaining in the next 30 days - """ - ) - - static let subscribeButtonText = NSLocalizedString( - "prepublishing.socialAccounts.footer.button.text", - value: "Subscribe to share more", - comment: "The label for a call-to-action button in the social accounts' footer section." - ) - - static let warningIconAccessibilityText = NSLocalizedString( - "prepublishing.socialAccounts.footer.warningIcon.accessibilityHint", - value: "Warning", - comment: "a VoiceOver description for the warning icon to hint that the remaining shares are low." - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift index 346c287c62a2..9799c0d556bb 100644 --- a/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift @@ -1,4 +1,5 @@ import UIKit +import JetpackSocial import WordPressData import WordPressKit import WordPressFlux @@ -507,11 +508,11 @@ extension SiteStatsInsightsTableViewController: SiteStatsInsightsDelegate { return } - guard let sharingVC = SharingViewController(blog: blog, delegate: self) else { - return + guard let manageVC = ManageConnectionsHostingController.make(for: blog) else { + return wpAssertionFailure("social connections service unavailable") } - let navigationController = UINavigationController(rootViewController: sharingVC) + let navigationController = UINavigationController(rootViewController: manageVC) present(navigationController, animated: true) applyTableUpdates() @@ -700,15 +701,6 @@ extension SiteStatsInsightsTableViewController: UIAdaptivePresentationController } } -// MARK: - SharingViewControllerDelegate - -extension SiteStatsInsightsTableViewController: SharingViewControllerDelegate { - func didChangePublicizeServices() { - markCurrentNudgeAsCompleted() - trackNudgeEvent(.statsPublicizeNudgeCompleted) - } -} - // MARK: - BloggingRemindersFlowDelegate extension SiteStatsInsightsTableViewController: BloggingRemindersFlowDelegate { diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ab53a122a86a..6d6a9b5c0415 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -835,7 +835,6 @@ Utility/Sharing/WPActivityDefaults.h, Utility/UIAlertControllerProxy.h, Utility/WPError.h, - ViewRelated/Blog/Sharing/SharingViewController.h, "ViewRelated/Blog/Site Settings/SettingTableViewCell.h", "ViewRelated/Blog/Site Settings/SiteSettingsViewController.h", ViewRelated/Comments/Controllers/CommentsViewController.h, From 30e08ea8b682aab4f63fd375db0b5e5c64624d6d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 16:04:09 +1200 Subject: [PATCH 5/9] Apply swift-format ahead of the legacy publicize services removal --- .../Swift/PublicizeInfo+CoreDataClass.swift | 4 +- .../WordPressKit/SharingServiceRemote.swift | 284 +++++++---- .../Tests/Services/PostCoordinatorTests.swift | 153 +++--- .../Tests/SharingServiceRemoteTests.swift | 30 +- .../Classes/Services/SharingService.swift | 477 +++++++++++------- 5 files changed, 585 insertions(+), 363 deletions(-) diff --git a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift index 4c4c0c557edd..fb3ba209d181 100644 --- a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift +++ b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift @@ -16,11 +16,11 @@ public class PublicizeInfo: NSManagedObject { } @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "PublicizeInfo") + NSFetchRequest(entityName: "PublicizeInfo") } @nonobjc public class func newObject(in context: NSManagedObjectContext) -> PublicizeInfo? { - return NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? PublicizeInfo + NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? PublicizeInfo } public func configure(with remote: RemotePublicizeInfo) { diff --git a/Modules/Sources/WordPressKit/SharingServiceRemote.swift b/Modules/Sources/WordPressKit/SharingServiceRemote.swift index 943d78465583..ef99e0392829 100644 --- a/Modules/Sources/WordPressKit/SharingServiceRemote.swift +++ b/Modules/Sources/WordPressKit/SharingServiceRemote.swift @@ -38,7 +38,10 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func getPublicizeServices(_ success: (([RemotePublicizeService]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func getPublicizeServices( + _ success: (([RemotePublicizeService]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "meta/external-services" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) let params = ["type": "publicize"] @@ -61,9 +64,11 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - siteID: The WordPress.com ID of the site. /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. /// - failure: An optional failure block accepting an `NSError` argument. - @objc open func getPublicizeServices(for siteID: NSNumber, - success: (([RemotePublicizeService]) -> Void)?, - failure: ((NSError?) -> Void)?) { + @objc open func getPublicizeServices( + for siteID: NSNumber, + success: (([RemotePublicizeService]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let path = path(forEndpoint: "sites/\(siteID)/external-services", withVersion: ._2_0) let params = ["type": "publicize" as AnyObject] @@ -94,7 +99,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { let endpoint = "me/keyring-connections" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - wordPressComRESTAPI.get(path, + wordPressComRESTAPI.get( + path, parameters: nil, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -112,15 +118,22 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { let dict = dict as AnyObject let externalUsers = dict.array(forKey: ConnectionDictionaryKeys.additionalExternalUsers) ?? [] conn.additionalExternalUsers = self.externalUsersForKeyringConnection(externalUsers as NSArray) - conn.dateExpires = WPKitDateUtils.date(fromISOString: dict.string(forKey: ConnectionDictionaryKeys.expires)) - conn.dateIssued = WPKitDateUtils.date(fromISOString: dict.string(forKey: ConnectionDictionaryKeys.issued)) - conn.externalDisplay = dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay + conn.dateExpires = WPKitDateUtils.date( + fromISOString: dict.string(forKey: ConnectionDictionaryKeys.expires) + ) + conn.dateIssued = WPKitDateUtils.date( + fromISOString: dict.string(forKey: ConnectionDictionaryKeys.issued) + ) + conn.externalDisplay = + dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay conn.externalID = dict.string(forKey: ConnectionDictionaryKeys.externalID) ?? conn.externalID conn.externalName = dict.string(forKey: ConnectionDictionaryKeys.externalName) ?? conn.externalName if conn.externalDisplay.isEmpty { conn.externalDisplay = conn.externalName } - conn.externalProfilePicture = dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture + conn.externalProfilePicture = + dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) + ?? conn.externalProfilePicture conn.keyringID = dict.number(forKey: ConnectionDictionaryKeys.ID) ?? conn.keyringID conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL @@ -136,7 +149,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Creates KeyringConnectionExternalUser instances from the past array of @@ -150,10 +164,16 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { private func externalUsersForKeyringConnection(_ externalUsers: NSArray) -> [KeyringConnectionExternalUser] { let arr: [KeyringConnectionExternalUser] = externalUsers.map { dict -> KeyringConnectionExternalUser in let externalUser = KeyringConnectionExternalUser() - externalUser.externalID = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalID) ?? externalUser.externalID - externalUser.externalName = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalName) ?? externalUser.externalName - externalUser.externalProfilePicture = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? externalUser.externalProfilePicture - externalUser.externalCategory = (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalCategory) ?? externalUser.externalCategory + externalUser.externalID = + (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalID) ?? externalUser.externalID + externalUser.externalName = + (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalName) ?? externalUser.externalName + externalUser.externalProfilePicture = + (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalProfilePicture) + ?? externalUser.externalProfilePicture + externalUser.externalCategory = + (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalCategory) + ?? externalUser.externalCategory return externalUser } @@ -167,11 +187,16 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemotePublicizeConnection` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func getPublicizeConnections(_ siteID: NSNumber, success: (([RemotePublicizeConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func getPublicizeConnections( + _ siteID: NSNumber, + success: (([RemotePublicizeConnection]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/publicize-connections" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - wordPressComRESTAPI.get(path, + wordPressComRESTAPI.get( + path, parameters: nil, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -184,7 +209,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { } let connections = responseDict.array(forKey: ConnectionDictionaryKeys.connections) ?? [] - let publicizeConnections: [RemotePublicizeConnection] = connections.compactMap { dict -> RemotePublicizeConnection? in + let publicizeConnections: [RemotePublicizeConnection] = connections.compactMap { + dict -> RemotePublicizeConnection? in let conn = self.remotePublicizeConnectionFromDictionary(dict as! NSDictionary) return conn } @@ -193,7 +219,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Create a new Publicize connection bweteen the specified blog and @@ -205,38 +232,43 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting a `RemotePublicizeConnection` object. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func createPublicizeConnection(_ siteID: NSNumber, + @objc open func createPublicizeConnection( + _ siteID: NSNumber, keyringConnectionID: NSNumber, externalUserID: String?, success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError) -> Void)?) { + failure: ((NSError) -> Void)? + ) { - let endpoint = "sites/\(siteID)/publicize-connections/new" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) + let endpoint = "sites/\(siteID)/publicize-connections/new" + let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - var parameters: [String: AnyObject] = [PublicizeConnectionParams.keyringConnectionID: keyringConnectionID] - if let userID = externalUserID { - parameters[PublicizeConnectionParams.externalUserID] = userID as AnyObject? - } + var parameters: [String: AnyObject] = [PublicizeConnectionParams.keyringConnectionID: keyringConnectionID] + if let userID = externalUserID { + parameters[PublicizeConnectionParams.externalUserID] = userID as AnyObject? + } - wordPressComRESTAPI.post(path, - parameters: parameters, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } + wordPressComRESTAPI.post( + path, + parameters: parameters, + success: { responseObject, httpResponse in + guard let onSuccess = success else { + return + } - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } + guard let responseDict = responseObject as? NSDictionary, + let conn = self.remotePublicizeConnectionFromDictionary(responseDict) + else { + failure?(self.errorForUnexpectedResponse(httpResponse)) + return + } - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) + onSuccess(conn) + }, + failure: { error, _ in + failure?(error as NSError) + } + ) } /// Update the shared status of the specified publicize connection @@ -250,37 +282,42 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting no arguments. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func updatePublicizeConnectionWithID(_ connectionID: NSNumber, + @objc open func updatePublicizeConnectionWithID( + _ connectionID: NSNumber, externalID: String?, forSite siteID: NSNumber, success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let externalUserID = (externalID == nil) ? "false" : externalID! - - let parameters = [ - PublicizeConnectionParams.externalUserID: externalUserID - ] - - wordPressComRESTAPI.post(path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } + failure: ((NSError?) -> Void)? + ) { + let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" + let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) + let externalUserID = (externalID == nil) ? "false" : externalID! - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } + let parameters = [ + PublicizeConnectionParams.externalUserID: externalUserID + ] + + wordPressComRESTAPI.post( + path, + parameters: parameters as [String: AnyObject]?, + success: { responseObject, httpResponse in + guard let onSuccess = success else { + return + } + + guard let responseDict = responseObject as? NSDictionary, + let conn = self.remotePublicizeConnectionFromDictionary(responseDict) + else { + failure?(self.errorForUnexpectedResponse(httpResponse)) + return + } - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) + onSuccess(conn) + }, + failure: { error, _ in + failure?(error as NSError) + } + ) } /// Update the shared status of the specified publicize connection @@ -292,35 +329,40 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting no arguments. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func updatePublicizeConnectionWithID(_ connectionID: NSNumber, + @objc open func updatePublicizeConnectionWithID( + _ connectionID: NSNumber, shared: Bool, forSite siteID: NSNumber, success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let parameters = [ - PublicizeConnectionParams.shared: shared - ] - - wordPressComRESTAPI.post(path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } + failure: ((NSError?) -> Void)? + ) { + let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" + let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) + let parameters = [ + PublicizeConnectionParams.shared: shared + ] - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } + wordPressComRESTAPI.post( + path, + parameters: parameters as [String: AnyObject]?, + success: { responseObject, httpResponse in + guard let onSuccess = success else { + return + } + + guard let responseDict = responseObject as? NSDictionary, + let conn = self.remotePublicizeConnectionFromDictionary(responseDict) + else { + failure?(self.errorForUnexpectedResponse(httpResponse)) + return + } - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - }) + onSuccess(conn) + }, + failure: { error, _ in + failure?(error as NSError) + } + ) } /// Disconnects (deletes) the specified publicize connection @@ -331,18 +373,25 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting no arguments. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func deletePublicizeConnection(_ siteID: NSNumber, connectionID: NSNumber, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func deletePublicizeConnection( + _ siteID: NSNumber, + connectionID: NSNumber, + success: (() -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)/delete" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - wordPressComRESTAPI.post(path, + wordPressComRESTAPI.post( + path, parameters: nil, success: { _, _ in success?() }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` @@ -364,10 +413,14 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { if conn.externalDisplay.isEmpty { conn.externalDisplay = conn.externalName } - conn.externalProfilePicture = dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture - conn.externalProfileURL = dict.string(forKey: ConnectionDictionaryKeys.externalProfileURL) ?? conn.externalProfileURL - conn.keyringConnectionID = dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionID) ?? conn.keyringConnectionID - conn.keyringConnectionUserID = dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionUserID) ?? conn.keyringConnectionUserID + conn.externalProfilePicture = + dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture + conn.externalProfileURL = + dict.string(forKey: ConnectionDictionaryKeys.externalProfileURL) ?? conn.externalProfileURL + conn.keyringConnectionID = + dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionID) ?? conn.keyringConnectionID + conn.keyringConnectionUserID = + dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionUserID) ?? conn.keyringConnectionUserID conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL conn.status = dict.string(forKey: ConnectionDictionaryKeys.status) ?? conn.status @@ -405,11 +458,16 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemoteSharingButton` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func getSharingButtonsForSite(_ siteID: NSNumber, success: (([RemoteSharingButton]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func getSharingButtonsForSite( + _ siteID: NSNumber, + success: (([RemoteSharingButton]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/sharing-buttons" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - wordPressComRESTAPI.get(path, + wordPressComRESTAPI.get( + path, parameters: nil, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -428,7 +486,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Updates the list of sharing buttons for a blog. @@ -439,13 +498,19 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { /// - success: An optional success block accepting an array of `RemoteSharingButton` objects. /// - failure: An optional failure block accepting an `NSError` argument. /// - @objc open func updateSharingButtonsForSite(_ siteID: NSNumber, sharingButtons: [RemoteSharingButton], success: (([RemoteSharingButton]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc open func updateSharingButtonsForSite( + _ siteID: NSNumber, + sharingButtons: [RemoteSharingButton], + success: (([RemoteSharingButton]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { let endpoint = "sites/\(siteID)/sharing-buttons" let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) let buttons = dictionariesFromRemoteSharingButtons(sharingButtons) let parameters = [SharingButtonsKeys.sharingButtons: buttons] - wordPressComRESTAPI.post(path, + wordPressComRESTAPI.post( + path, parameters: parameters as [String: AnyObject]?, success: { responseObject, httpResponse in guard let onSuccess = success else { @@ -464,7 +529,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { }, failure: { error, _ in failure?(error as NSError) - }) + } + ) } /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` @@ -497,7 +563,7 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { } private func dictionariesFromRemoteSharingButtons(_ buttons: [RemoteSharingButton]) -> [NSDictionary] { - return buttons.map({ btn -> NSDictionary in + buttons.map({ btn -> NSDictionary in let dict = NSMutableDictionary() dict[SharingButtonsKeys.buttonID] = btn.buttonID @@ -515,7 +581,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { private func remotePublicizeServicesFromDictionary(_ dictionary: NSDictionary) -> [RemotePublicizeService] { let responseString = dictionary.description as NSString - let services: NSDictionary = (dictionary.forKey(ServiceDictionaryKeys.services) as? NSDictionary) ?? NSDictionary() + let services: NSDictionary = + (dictionary.forKey(ServiceDictionaryKeys.services) as? NSDictionary) ?? NSDictionary() return services.allKeys.map { key in let dict = (services.forKey(key) as? NSDictionary) ?? NSDictionary() @@ -529,7 +596,8 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { pub.jetpackModuleRequired = dict.string(forKey: ServiceDictionaryKeys.jetpackModuleRequired) ?? "" pub.jetpackSupport = dict.number(forKey: ServiceDictionaryKeys.jetpackSupport)?.boolValue ?? false pub.label = dict.string(forKey: ServiceDictionaryKeys.label) ?? "" - pub.multipleExternalUserIDSupport = dict.number(forKey: ServiceDictionaryKeys.multipleExternalUserIDSupport)?.boolValue ?? false + pub.multipleExternalUserIDSupport = + dict.number(forKey: ServiceDictionaryKeys.multipleExternalUserIDSupport)?.boolValue ?? false pub.type = dict.string(forKey: ServiceDictionaryKeys.type) ?? "" pub.status = dict.string(forKey: ServiceDictionaryKeys.status) ?? "" diff --git a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift index 29319a7fcd44..d4c1991809da 100644 --- a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift +++ b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift @@ -161,7 +161,10 @@ class PostCoordinatorTests: CoreDataTestCase { media.blog = post.blog media.mediaType = .image media.filename = "test-image.jpg" - media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL(forResource: "test-image", fileExtension: "jpg") + media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL( + forResource: "test-image", + fileExtension: "jpg" + ) // important otherwise MediaService will use temporary objectID and fail try mainContext.save() @@ -170,7 +173,8 @@ class PostCoordinatorTests: CoreDataTestCase { revision1.postTitle = "title-b" revision1.media = [media] let uploadID = media.gutenbergUploadID - revision1.content = "\n
\n" + revision1.content = + "\n
\n" // GIVEN stub(condition: isPath("/rest/v1.2/sites/80511/posts/new")) { request in @@ -191,7 +195,10 @@ class PostCoordinatorTests: CoreDataTestCase { try await coordinator.waitForSync(post, to: revision1) // THEN image block was updated - XCTAssertEqual(post.content, "\n
\n") + XCTAssertEqual( + post.content, + "\n
\n" + ) // THEN revisions were uploaded XCTAssertFalse(post.hasRevision()) @@ -231,11 +238,17 @@ class PostCoordinatorTests: CoreDataTestCase { coordinator.syncRetryDelay = 10 // GIVEN the app quickly restoring connectivity after the first failure - coordinator.syncEvents.sink { - if case .finished(_, let result) = $0, case .failure = result { - NotificationCenter.default.post(name: .reachabilityUpdated, object: nil, userInfo: [Foundation.Notification.reachabilityKey: true]) + coordinator.syncEvents + .sink { + if case .finished(_, let result) = $0, case .failure = result { + NotificationCenter.default.post( + name: .reachabilityUpdated, + object: nil, + userInfo: [Foundation.Notification.reachabilityKey: true] + ) + } } - }.store(in: &cancellables) + .store(in: &cancellables) coordinator.setNeedsSync(for: revision1) try await coordinator.waitForSync(post, to: revision1, ignoreErrors: true) @@ -260,7 +273,10 @@ class PostCoordinatorTests: CoreDataTestCase { media.blog = post.blog media.mediaType = .image media.filename = "test-image.jpg" - media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL(forResource: "test-image", fileExtension: "jpg") + media.absoluteLocalURL = try MediaImageServiceTests.makeLocalURL( + forResource: "test-image", + fileExtension: "jpg" + ) // important otherwise MediaService will use temporary objectID and fail try mainContext.save() @@ -269,7 +285,8 @@ class PostCoordinatorTests: CoreDataTestCase { revision1.postTitle = "title-b" revision1.media = [media] let uploadID = media.gutenbergUploadID - revision1.content = "\n
\n" + revision1.content = + "\n
\n" // GIVEN let expectation = self.expectation(description: "started-media-request") @@ -316,10 +333,13 @@ class PostCoordinatorTests: CoreDataTestCase { // GIVEN a server where the post was deleted stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { _ in - return try! HTTPStubsResponse(value: [ - "error": "unknown_post", - "message": "Unknown post" - ], statusCode: 404) + try! HTTPStubsResponse( + value: [ + "error": "unknown_post", + "message": "Unknown post" + ], + statusCode: 404 + ) } // WHEN @@ -329,7 +349,8 @@ class PostCoordinatorTests: CoreDataTestCase { XCTFail("Expected sync to fail") } catch { guard let error = error as? PostRepository.PostSaveError, - case .deleted = error else { + case .deleted = error + else { return XCTFail("Unexpected error") } } @@ -393,10 +414,13 @@ class PostCoordinatorTests: CoreDataTestCase { return response } stub(condition: isPath("/rest/v1.2/sites/80511/posts/974")) { request in - XCTAssertEqual(request.getBodyParameters(), [ - "slug": "hello", - "status": "publish" - ]) + XCTAssertEqual( + request.getBodyParameters(), + [ + "slug": "hello", + "status": "publish" + ] + ) var post = WordPressComPost.mock post.status = AbstractPost.Status.publish.rawValue post.title = "title-a" @@ -445,18 +469,21 @@ class PostCoordinatorTests: CoreDataTestCase { let parameters = request.getBodyParameters() ?? [:] XCTAssertEqual(parameters["status"], "publish") let metadata = (parameters["metadata"] as? [[String: AnyHashable]]) ?? [] - XCTAssertEqual(Set(metadata), Set([ - [ - "key": "_wpas_mess", - "operation": "update", - "value": "message-a" - ], - [ - "key": "_jetpack_blogging_prompt_key", - "operation": "update", - "value": "prompt-a" - ] - ])) + XCTAssertEqual( + Set(metadata), + Set([ + [ + "key": "_wpas_mess", + "operation": "update", + "value": "message-a" + ], + [ + "key": "_jetpack_blogging_prompt_key", + "operation": "update", + "value": "prompt-a" + ] + ]) + ) var post = WordPressComPost.mock post.metadata = [ WordPressComPost.Metadata(id: "752", key: "_wpas_mess", value: "message-a") @@ -593,36 +620,37 @@ class PostCoordinatorTests: CoreDataTestCase { } private let mediaResponse = """ -{ - "media": [ { - "ID": 1236, - "URL": "https://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", - "guid": "http://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", - "date": "2024-03-25T18:50:12-04:00", - "post_ID": 0, - "author_ID": 34129043, - "file": "img_0005-1-1.jpg", - "mime_type": "image/jpeg", - "extension": "jpg", - "title": "img_0005-1", - "caption": "", - "description": "", - "alt": "", - "icon": "https://s1.wp.com/wp-includes/images/media/default.png", - "size": "701.54 KB", - "height": 1335, - "width": 2000 + "media": [ + { + "ID": 1236, + "URL": "https://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", + "guid": "http://example.files.wordpress.com/2024/03/img_0005-1-1.jpg", + "date": "2024-03-25T18:50:12-04:00", + "post_ID": 0, + "author_ID": 34129043, + "file": "img_0005-1-1.jpg", + "mime_type": "image/jpeg", + "extension": "jpg", + "title": "img_0005-1", + "caption": "", + "description": "", + "alt": "", + "icon": "https://s1.wp.com/wp-includes/images/media/default.png", + "size": "701.54 KB", + "height": 1335, + "width": 2000 + } + ] } - ] -} -""" + """ private extension URLRequest { func getBodyParameters() -> [String: AnyHashable]? { guard let data = httpBodyStream?.read(), - let object = try? JSONSerialization.jsonObject(with: data), - let parameters = object as? [String: AnyHashable] else { + let object = try? JSONSerialization.jsonObject(with: data), + let parameters = object as? [String: AnyHashable] + else { return nil } return parameters @@ -630,7 +658,12 @@ private extension URLRequest { } private extension PostCoordinator { - func waitForSync(_ post: AbstractPost, to revision: AbstractPost, ignoreErrors: Bool = false, timeout: TimeInterval = 5) async throws { + func waitForSync( + _ post: AbstractPost, + to revision: AbstractPost, + ignoreErrors: Bool = false, + timeout: TimeInterval = 5 + ) async throws { var olderRevisionIDs = Set(post.allRevisions.filter(\.isSyncNeeded).map(\.objectID)) olderRevisionIDs.remove(revision.objectID) return try await waitForSync(post, ignoreErrors: ignoreErrors, timeout: timeout) { operation in @@ -649,8 +682,14 @@ private extension PostCoordinator { /// /// - parameter timeout: The default value is 5 seconds. /// - parameter handler: Return `true` if the revision matches the one you expected. - func waitForSync(_ post: AbstractPost, ignoreErrors: Bool = false, timeout: TimeInterval = 5, handler: @escaping (PostCoordinator.SyncOperation) -> Bool) async throws { - let result = await syncEvents + func waitForSync( + _ post: AbstractPost, + ignoreErrors: Bool = false, + timeout: TimeInterval = 5, + handler: @escaping (PostCoordinator.SyncOperation) -> Bool + ) async throws { + let result = + await syncEvents .compactMap { event -> Result? in guard case .finished(let operation, let result) = event else { return nil diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift index 919f67aa7550..2b2199ad280f 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift @@ -45,7 +45,7 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { } XCTAssertTrue(facebookService.status.isEmpty) - guard let twitterService = publicizeServices.first(where: { $0.serviceID == "twitter"}) else { + guard let twitterService = publicizeServices.first(where: { $0.serviceID == "twitter" }) else { XCTFail("Expected a RemotePublicizeService to exist") return } @@ -82,11 +82,13 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { let mockID = NSNumber(value: 10) let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections/new", withVersion: ._1_1) - service.createPublicizeConnection(mockID, - keyringConnectionID: mockID, - externalUserID: nil, - success: nil, - failure: nil) + service.createPublicizeConnection( + mockID, + keyringConnectionID: mockID, + externalUserID: nil, + success: nil, + failure: nil + ) XCTAssertTrue(api.postMethodCalled, "Method was not called") XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") @@ -94,8 +96,10 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { func testDeletePublicizeConnection() { let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections/\(mockID)/delete", - withVersion: ._1_1) + let url = service.path( + forEndpoint: "sites/\(mockID)/publicize-connections/\(mockID)/delete", + withVersion: ._1_1 + ) service.deletePublicizeConnection(mockID, connectionID: mockID, success: nil, failure: nil) @@ -117,10 +121,12 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { let mockID = NSNumber(value: 10) let url = service.path(forEndpoint: "sites/\(mockID)/sharing-buttons", withVersion: ._1_1) - service.updateSharingButtonsForSite(mockID, - sharingButtons: [RemoteSharingButton](), - success: nil, - failure: nil) + service.updateSharingButtonsForSite( + mockID, + sharingButtons: [RemoteSharingButton](), + success: nil, + failure: nil + ) XCTAssertTrue(api.postMethodCalled, "Method was not called") XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") diff --git a/WordPress/Classes/Services/SharingService.swift b/WordPress/Classes/Services/SharingService.swift index 51b771c4d6fc..1d4885f1ce6a 100644 --- a/WordPress/Classes/Services/SharingService.swift +++ b/WordPress/Classes/Services/SharingService.swift @@ -32,17 +32,23 @@ import WordPressKit /// - success: An optional success block accepting no parameters /// - failure: An optional failure block accepting an `NSError` parameter /// - @objc public func syncPublicizeServicesForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + @objc public func syncPublicizeServicesForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) + { guard let remote = remoteForBlog(blog), - let blogID = blog.dotComID else { + let blogID = blog.dotComID + else { failure?(nil) return } - remote.getPublicizeServices(for: blogID, success: { remoteServices in - // Process the results - self.mergePublicizeServices(remoteServices, success: success) - }, failure: failure) + remote.getPublicizeServices( + for: blogID, + success: { remoteServices in + // Process the results + self.mergePublicizeServices(remoteServices, success: success) + }, + failure: failure + ) } /// Fetches the current user's list of keyring connections. Nothing is saved to core data. @@ -53,15 +59,21 @@ import WordPressKit /// - success: An optional success block accepting an array of `KeyringConnection` objects /// - failure: An optional failure block accepting an `NSError` parameter /// - @objc public func fetchKeyringConnectionsForBlog(_ blog: Blog, success: (([KeyringConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { + @objc public func fetchKeyringConnectionsForBlog( + _ blog: Blog, + success: (([KeyringConnection]) -> Void)?, + failure: ((NSError?) -> Void)? + ) { guard let remote = remoteForBlog(blog) else { return } - remote.getKeyringConnections({ keyringConnections in - // Just return the result - success?(keyringConnections) - }, - failure: failure) + remote.getKeyringConnections( + { keyringConnections in + // Just return the result + success?(keyringConnections) + }, + failure: failure + ) } /// Creates a new publicize connection for the specified `Blog`, using the specified @@ -74,11 +86,13 @@ import WordPressKit /// - success: An optional success block accepting a `PublicizeConnection` parameter. /// - failure: An optional failure block accepting an NSError parameter. /// - @objc public func createPublicizeConnectionForBlog(_ blog: Blog, + @objc public func createPublicizeConnectionForBlog( + _ blog: Blog, keyring: KeyringConnection, externalUserID: String?, success: ((PublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)?) { + failure: ((NSError?) -> Void)? + ) { let blogObjectID = blog.objectID guard let remote = remoteForBlog(blog) else { return @@ -94,27 +108,36 @@ import WordPressKit ] WPAppAnalytics.track(.sharingPublicizeConnected, properties: properties, blogID: dotComID) - self.coreDataStack.performAndSave({ context -> NSManagedObjectID in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in - let transformed = result.flatMap { objectID in - Result { - let object = try self.coreDataStack.mainContext.existingObject(with: objectID) - return object as! PublicizeConnection + self.coreDataStack.performAndSave( + { context -> NSManagedObjectID in + try self.createOrReplacePublicizeConnectionForBlogWithObjectID( + blogObjectID, + remoteConnection: remoteConnection, + in: context + ) + }, + completion: { result in + let transformed = result.flatMap { objectID in + Result { + let object = try self.coreDataStack.mainContext.existingObject(with: objectID) + return object as! PublicizeConnection + } } - } - switch transformed { - case let .success(object): - success?(object) - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, on: .main) + switch transformed { + case let .success(object): + success?(object) + case let .failure(error): + DDLogError("Error creating publicize connection from remote: \(error)") + failure?(error as NSError) + } + }, + on: .main + ) }, failure: { (error: NSError?) in failure?(error) - }) + } + ) } /// Update the specified `PublicizeConnection`. The update to core data is performed @@ -133,65 +156,89 @@ import WordPressKit success: (() -> Void)?, failure: ((NSError?) -> Void)? ) { - typealias PubConnUpdateResult = (oldValue: Bool, siteID: NSNumber, connectionID: NSNumber, service: String, remote: SharingServiceRemote?) + typealias PubConnUpdateResult = ( + oldValue: Bool, siteID: NSNumber, connectionID: NSNumber, service: String, remote: SharingServiceRemote? + ) let blogObjectID = blog.objectID - coreDataStack.performAndSave({ context -> PubConnUpdateResult in - let blogInContext = try context.existingObject(with: blogObjectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - let oldValue = pubConnInContext.shared - pubConnInContext.shared = shared - return ( - oldValue: oldValue, - siteID: pubConnInContext.siteID, - connectionID: pubConnInContext.connectionID, - service: pubConnInContext.service, - remote: self.remoteForBlog(blogInContext) - ) - }, completion: { result in - switch result { - case let .success(value): - if value.oldValue == shared { - success?() - return - } + coreDataStack.performAndSave( + { context -> PubConnUpdateResult in + let blogInContext = try context.existingObject(with: blogObjectID) as! Blog + let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection + let oldValue = pubConnInContext.shared + pubConnInContext.shared = shared + return ( + oldValue: oldValue, + siteID: pubConnInContext.siteID, + connectionID: pubConnInContext.connectionID, + service: pubConnInContext.service, + remote: self.remoteForBlog(blogInContext) + ) + }, + completion: { result in + switch result { + case let .success(value): + if value.oldValue == shared { + success?() + return + } - value.remote?.updatePublicizeConnectionWithID( - value.connectionID, - shared: shared, - forSite: value.siteID, - success: { remoteConnection in - let properties = [ - "service": value.service, - "is_site_wide": NSNumber(value: shared).stringValue - ] - WPAppAnalytics.track(.sharingPublicizeConnectionAvailableToAllChanged, properties: properties, blogID: value.siteID) - - self.coreDataStack.performAndSave({ context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in - switch result { - case .success: - success?() - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) + value.remote? + .updatePublicizeConnectionWithID( + value.connectionID, + shared: shared, + forSite: value.siteID, + success: { remoteConnection in + let properties = [ + "service": value.service, + "is_site_wide": NSNumber(value: shared).stringValue + ] + WPAppAnalytics.track( + .sharingPublicizeConnectionAvailableToAllChanged, + properties: properties, + blogID: value.siteID + ) + + self.coreDataStack.performAndSave( + { context in + try self.createOrReplacePublicizeConnectionForBlogWithObjectID( + blogObjectID, + remoteConnection: remoteConnection, + in: context + ) + }, + completion: { result in + switch result { + case .success: + success?() + case let .failure(error): + DDLogError("Error creating publicize connection from remote: \(error)") + failure?(error as NSError) + } + }, + on: .main + ) + }, + failure: { (error: NSError?) in + self.coreDataStack.performAndSave( + { context in + let pubConnInContext = + try context.existingObject(with: pubConn.objectID) as! PublicizeConnection + pubConnInContext.shared = value.oldValue + }, + completion: { _ in + failure?(error) + }, + on: .main + ) } - }, on: .main) - }, - failure: { (error: NSError?) in - self.coreDataStack.performAndSave({ context in - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - pubConnInContext.shared = value.oldValue - }, completion: { _ in - failure?(error) - }, on: .main) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, on: .main) + ) + case let .failure(error): + failure?(error as NSError) + } + }, + on: .main + ) } /// Update the specified `PublicizeConnection`. The update to core data is performed @@ -203,28 +250,37 @@ import WordPressKit /// - success: An optional success block accepting no parameters. /// - failure: An optional failure block accepting an NSError parameter. /// - @objc public func updateExternalID(_ externalID: String, + @objc public func updateExternalID( + _ externalID: String, forBlog blog: Blog, forPublicizeConnection pubConn: PublicizeConnection, success: (() -> Void)?, - failure: ((NSError?) -> Void)?) { - if pubConn.externalID == externalID { - success?() - return - } + failure: ((NSError?) -> Void)? + ) { + if pubConn.externalID == externalID { + success?() + return + } - let blogObjectID = blog.objectID - let siteID = pubConn.siteID - guard let remote = remoteForBlog(blog) else { - return - } - remote.updatePublicizeConnectionWithID(pubConn.connectionID, - externalID: externalID, - forSite: siteID, - success: { remoteConnection in - self.coreDataStack.performAndSave({ context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID(blogObjectID, remoteConnection: remoteConnection, in: context) - }, completion: { result in + let blogObjectID = blog.objectID + let siteID = pubConn.siteID + guard let remote = remoteForBlog(blog) else { + return + } + remote.updatePublicizeConnectionWithID( + pubConn.connectionID, + externalID: externalID, + forSite: siteID, + success: { remoteConnection in + self.coreDataStack.performAndSave( + { context in + try self.createOrReplacePublicizeConnectionForBlogWithObjectID( + blogObjectID, + remoteConnection: remoteConnection, + in: context + ) + }, + completion: { result in switch result { case .success: success?() @@ -232,9 +288,12 @@ import WordPressKit DDLogError("Error creating publicize connection from remote: \(error)") failure?(error as NSError) } - }, on: .main) - }, - failure: failure) + }, + on: .main + ) + }, + failure: failure + ) } /// Deletes the specified `PublicizeConnection`. The delete from core data is performed @@ -246,45 +305,61 @@ import WordPressKit /// - success: An optional success block accepting no parameters. /// - failure: An optional failure block accepting an NSError parameter. /// - @objc public func deletePublicizeConnectionForBlog(_ blog: Blog, pubConn: PublicizeConnection, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + @objc public func deletePublicizeConnectionForBlog( + _ blog: Blog, + pubConn: PublicizeConnection, + success: (() -> Void)?, + failure: ((NSError?) -> Void)? + ) { // optimistically delete the connection locally. - coreDataStack.performAndSave({ context in - let blogInContext = try context.existingObject(with: blog.objectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - - let siteID = pubConnInContext.siteID - context.delete(pubConnInContext) - return (siteID, pubConnInContext.connectionID, pubConnInContext.service, self.remoteForBlog(blogInContext)) - }, completion: { result in - switch result { - case let .success((siteID, connectionID, service, remote)): - remote?.deletePublicizeConnection( - siteID, - connectionID: connectionID, - success: { - let properties = [ - "service": service - ] - WPAppAnalytics.track(.sharingPublicizeDisconnected, properties: properties, blogID: siteID) - success?() - }, - failure: { (error: NSError?) in - if let errorCode = error?.userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String { - if errorCode == self.SharingAPIErrorNotFound { - // This is a special situation. If the call to disconnect the service returns not_found then the service - // has probably already been disconnected and the call was made with stale data. - // Assume this is the case and treat this error as a successful disconnect. + coreDataStack.performAndSave( + { context in + let blogInContext = try context.existingObject(with: blog.objectID) as! Blog + let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection + + let siteID = pubConnInContext.siteID + context.delete(pubConnInContext) + return ( + siteID, pubConnInContext.connectionID, pubConnInContext.service, self.remoteForBlog(blogInContext) + ) + }, + completion: { result in + switch result { + case let .success((siteID, connectionID, service, remote)): + remote? + .deletePublicizeConnection( + siteID, + connectionID: connectionID, + success: { + let properties = [ + "service": service + ] + WPAppAnalytics.track( + .sharingPublicizeDisconnected, + properties: properties, + blogID: siteID + ) success?() - return + }, + failure: { (error: NSError?) in + if let errorCode = error?.userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String { + if errorCode == self.SharingAPIErrorNotFound { + // This is a special situation. If the call to disconnect the service returns not_found then the service + // has probably already been disconnected and the call was made with stale data. + // Assume this is the case and treat this error as a successful disconnect. + success?() + return + } + } + failure?(error) } - } - failure?(error) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, on: .main) + ) + case let .failure(error): + failure?(error as NSError) + } + }, + on: .main + ) } // MARK: - Private PublicizeService Methods @@ -296,22 +371,26 @@ import WordPressKit /// - remoteServices: An array of `RemotePublicizeService` objects to merge. /// - success: An optional callback block to be performed when core data has saved the changes. /// - private func mergePublicizeServices(_ remoteServices: [RemotePublicizeService], success: (() -> Void)? ) { - coreDataStack.performAndSave({ context in - let currentPublicizeServices = (try? PublicizeService.allPublicizeServices(in: context)) ?? [] + private func mergePublicizeServices(_ remoteServices: [RemotePublicizeService], success: (() -> Void)?) { + coreDataStack.performAndSave( + { context in + let currentPublicizeServices = (try? PublicizeService.allPublicizeServices(in: context)) ?? [] - // Create or update based on the contents synced. - let servicesToKeep = remoteServices.map { remoteService -> PublicizeService in - self.createOrReplaceFromRemotePublicizeService(remoteService, in: context) - } + // Create or update based on the contents synced. + let servicesToKeep = remoteServices.map { remoteService -> PublicizeService in + self.createOrReplaceFromRemotePublicizeService(remoteService, in: context) + } - // Delete any cached PublicizeServices that were not synced. - for pubService in currentPublicizeServices { - if !servicesToKeep.contains(pubService) { - context.delete(pubService) + // Delete any cached PublicizeServices that were not synced. + for pubService in currentPublicizeServices { + if !servicesToKeep.contains(pubService) { + context.delete(pubService) + } } - } - }, completion: success, on: .main) + }, + completion: success, + on: .main + ) } /// Composes a new `PublicizeService`, or updates an existing one, with data represented by the passed `RemotePublicizeService`. @@ -320,11 +399,17 @@ import WordPressKit /// /// - Returns: A `PublicizeService`. /// - private func createOrReplaceFromRemotePublicizeService(_ remoteService: RemotePublicizeService, in context: NSManagedObjectContext) -> PublicizeService { + private func createOrReplaceFromRemotePublicizeService( + _ remoteService: RemotePublicizeService, + in context: NSManagedObjectContext + ) -> PublicizeService { var pubService = try? PublicizeService.lookupPublicizeServiceNamed(remoteService.serviceID, in: context) if pubService == nil { - pubService = NSEntityDescription.insertNewObject(forEntityName: PublicizeService.entityName(), - into: context) as? PublicizeService + pubService = + NSEntityDescription.insertNewObject( + forEntityName: PublicizeService.entityName(), + into: context + ) as? PublicizeService } pubService?.connectURL = remoteService.connectURL pubService?.detail = remoteService.detail @@ -380,11 +465,13 @@ import WordPressKit return } - remote.getSharingButtonsForSite(blog.dotComID!, + remote.getSharingButtonsForSite( + blog.dotComID!, success: { (remoteButtons: [RemoteSharingButton]) in self.mergeSharingButtonsForBlog(blogObjectID, remoteSharingButtons: remoteButtons, onComplete: success) }, - failure: failure) + failure: failure + ) } /// Pushes changes to the specified blog's `SharingButton`s back up to the blog. @@ -395,18 +482,25 @@ import WordPressKit /// - success: An optional success block accepting no parameters. /// - failure: An optional failure block accepting an `NSError` parameter. /// - @objc public func updateSharingButtonsForBlog(_ blog: Blog, sharingButtons: [SharingButton], success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + @objc public func updateSharingButtonsForBlog( + _ blog: Blog, + sharingButtons: [SharingButton], + success: (() -> Void)?, + failure: ((NSError?) -> Void)? + ) { let blogObjectID = blog.objectID guard let remote = remoteForBlog(blog) else { return } - remote.updateSharingButtonsForSite(blog.dotComID!, + remote.updateSharingButtonsForSite( + blog.dotComID!, sharingButtons: remoteShareButtonsFromShareButtons(sharingButtons), success: { (remoteButtons: [RemoteSharingButton]) in self.mergeSharingButtonsForBlog(blogObjectID, remoteSharingButtons: remoteButtons, onComplete: success) }, - failure: failure) + failure: failure + ) } /// Called when syncing sharing buttons. Merges synced and cached data, removing @@ -417,26 +511,34 @@ import WordPressKit /// - remoteSharingButtons: An array of `RemoteSharingButton` objects to merge. /// - onComplete: An optional callback block to be performed when core data has saved the changes. /// - private func mergeSharingButtonsForBlog(_ blogObjectID: NSManagedObjectID, remoteSharingButtons: [RemoteSharingButton], onComplete: (() -> Void)?) { - coreDataStack.performAndSave({ context in - let blog = try context.existingObject(with: blogObjectID) as! Blog + private func mergeSharingButtonsForBlog( + _ blogObjectID: NSManagedObjectID, + remoteSharingButtons: [RemoteSharingButton], + onComplete: (() -> Void)? + ) { + coreDataStack.performAndSave( + { context in + let blog = try context.existingObject(with: blogObjectID) as! Blog - let currentSharingbuttons = try SharingButton.allSharingButtons(for: blog, in: context) + let currentSharingbuttons = try SharingButton.allSharingButtons(for: blog, in: context) - // Create or update based on the contents synced. - let buttonsToKeep = remoteSharingButtons.map { remoteButton -> SharingButton in - return self.createOrReplaceFromRemoteSharingButton(remoteButton, blog: blog, in: context) - } + // Create or update based on the contents synced. + let buttonsToKeep = remoteSharingButtons.map { remoteButton -> SharingButton in + self.createOrReplaceFromRemoteSharingButton(remoteButton, blog: blog, in: context) + } - // Delete any cached PublicizeServices that were not synced. - for button in currentSharingbuttons { - if !buttonsToKeep.contains(button) { - context.delete(button) + // Delete any cached PublicizeServices that were not synced. + for button in currentSharingbuttons { + if !buttonsToKeep.contains(button) { + context.delete(button) + } } - } - }, completion: { _ in - onComplete?() - }, on: .main) + }, + completion: { _ in + onComplete?() + }, + on: .main + ) } /// Composes a new `SharingButton`, or updates an existing one, with @@ -448,11 +550,18 @@ import WordPressKit /// /// - Returns: A `SharingButton`. /// - private func createOrReplaceFromRemoteSharingButton(_ remoteButton: RemoteSharingButton, blog: Blog, in context: NSManagedObjectContext) -> SharingButton { + private func createOrReplaceFromRemoteSharingButton( + _ remoteButton: RemoteSharingButton, + blog: Blog, + in context: NSManagedObjectContext + ) -> SharingButton { var shareButton = try? SharingButton.lookupSharingButton(byID: remoteButton.buttonID, for: blog, in: context) if shareButton == nil { - shareButton = NSEntityDescription.insertNewObject(forEntityName: SharingButton.entityName(), - into: context) as? SharingButton + shareButton = + NSEntityDescription.insertNewObject( + forEntityName: SharingButton.entityName(), + into: context + ) as? SharingButton } shareButton?.buttonID = remoteButton.buttonID @@ -475,7 +584,7 @@ import WordPressKit /// - Returns: An array of `RemoteSharingButton` objects. /// private func remoteShareButtonsFromShareButtons(_ shareButtons: [SharingButton]) -> [RemoteSharingButton] { - return shareButtons.map { shareButton -> RemoteSharingButton in + shareButtons.map { shareButton -> RemoteSharingButton in let btn = RemoteSharingButton() btn.buttonID = shareButton.buttonID btn.name = shareButton.name From 23297f12e3c339834672f23cfc9ef69801796078 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 16:04:27 +1200 Subject: [PATCH 6/9] Remove the legacy publicize services and remotes Deletes the keyring-based connection sync (SharingSyncService), the publicize half of SharingService/SharingServiceRemote, the share-limit sync (Jetpack Social no longer has per-post share limits), and the legacy publicize merge in PostHelper. The sharing-buttons half of both services is kept. Also removes the now-orphaned PublicizeInfo.configure(with:) helper in WordPressData, whose only caller was the deleted JetpackSocialService. --- .../Swift/PublicizeInfo+CoreDataClass.swift | 8 - .../JetpackSocialServiceRemote.swift | 35 -- .../WordPressKit/KeyringConnection.swift | 23 - .../KeyringConnectionExternalUser.swift | 11 - .../RemotePublicizeConnection.swift | 22 - .../WordPressKit/RemotePublicizeInfo.swift | 15 - .../WordPressKit/RemotePublicizeService.swift | 16 - .../WordPressKit/SharingServiceRemote.swift | 509 +----------------- .../Tests/Models/Blog+PublicizeTests.swift | 68 --- .../Models/Post+JetpackSocialTests.swift | 285 ---------- .../Services/JetpackSocialServiceTests.swift | 231 -------- .../Tests/Services/PostCoordinatorTests.swift | 4 - .../PostHelperJetpackSocialTests.swift | 204 ------- .../Tests/Services/SharingServiceTests.swift | 84 --- .../Mock Data/jetpack-social-403.json | 7 - .../jetpack-social-no-publicize.json | 5 - .../jetpack-social-with-publicize.json | 10 - .../Mock Data/sites-external-services.json | 37 -- .../Tests/SharingServiceRemoteTests.swift | 91 ---- .../JetpackSocialServiceRemoteTests.swift | 79 --- .../Models/Blog/Blog+JetpackSocial.swift | 31 -- .../Models/PublicizeConnection+Creation.swift | 52 -- .../Models/PublicizeService+Lookup.swift | 48 -- WordPress/Classes/Services/BlogService.m | 31 -- .../Services/JetpackSocialService.swift | 111 ---- .../Services/PostHelper+JetpackSocial.swift | 121 ----- WordPress/Classes/Services/PostHelper.m | 8 - .../Classes/Services/SharingService.swift | 431 +-------------- .../Classes/Services/SharingSyncService.swift | 124 ----- .../BuildInformation/RemoteFeatureFlag.swift | 7 - 30 files changed, 3 insertions(+), 2705 deletions(-) delete mode 100644 Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift delete mode 100644 Modules/Sources/WordPressKit/KeyringConnection.swift delete mode 100644 Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift delete mode 100644 Modules/Sources/WordPressKit/RemotePublicizeConnection.swift delete mode 100644 Modules/Sources/WordPressKit/RemotePublicizeInfo.swift delete mode 100644 Modules/Sources/WordPressKit/RemotePublicizeService.swift delete mode 100644 Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift delete mode 100644 Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift delete mode 100644 Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift delete mode 100644 Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift delete mode 100644 Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift delete mode 100644 Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json delete mode 100644 Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json delete mode 100644 Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json delete mode 100644 Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json delete mode 100644 Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift delete mode 100644 WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift delete mode 100644 WordPress/Classes/Models/PublicizeConnection+Creation.swift delete mode 100644 WordPress/Classes/Models/PublicizeService+Lookup.swift delete mode 100644 WordPress/Classes/Services/JetpackSocialService.swift delete mode 100644 WordPress/Classes/Services/PostHelper+JetpackSocial.swift delete mode 100644 WordPress/Classes/Services/SharingSyncService.swift diff --git a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift index fb3ba209d181..05a33dc0e11e 100644 --- a/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift +++ b/Modules/Sources/WordPressData/Swift/PublicizeInfo+CoreDataClass.swift @@ -1,6 +1,5 @@ import Foundation import CoreData -import WordPressKit /// `PublicizeInfo` encapsulates the information related to Jetpack Social auto-sharing. /// @@ -23,13 +22,6 @@ public class PublicizeInfo: NSManagedObject { NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? PublicizeInfo } - public func configure(with remote: RemotePublicizeInfo) { - self.shareLimit = Int64(remote.shareLimit) - self.toBePublicizedCount = Int64(remote.toBePublicizedCount) - self.sharedPostsCount = Int64(remote.sharedPostsCount) - self.sharesRemaining = Int64(remote.sharesRemaining) - } - /// A value-type representation for Publicize auto-sharing usage. public struct SharingLimit: Hashable { /// The remaining shares available for use. diff --git a/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift b/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift deleted file mode 100644 index 7e732f3ce53b..000000000000 --- a/Modules/Sources/WordPressKit/JetpackSocialServiceRemote.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import WordPressKitObjC - -/// Encapsulates remote service logic related to Jetpack Social. -public class JetpackSocialServiceRemote: ServiceRemoteWordPressComREST { - - /// Retrieves the Publicize information for the given site. - /// - /// Note: Sites with disabled share limits will return success with nil value. - /// - /// - Parameters: - /// - siteID: The target site's dotcom ID. - /// - completion: Closure to be called once the request completes. - public func fetchPublicizeInfo(for siteID: Int, - completion: @escaping (Result) -> Void) { - let path = path(forEndpoint: "sites/\(siteID)/jetpack-social", withVersion: ._2_0) - Task { @MainActor in - await self.wordPressComRestApi - .perform( - .get, - URLString: path, - jsonDecoder: .apiDecoder, - type: RemotePublicizeInfo.self - ) - .map { $0.body } - .flatMapError { original -> Result in - if case let .endpointError(endpointError) = original, endpointError.response?.statusCode == 200, endpointError.code == .responseSerializationFailed { - return .success(nil) - } - return .failure(original.asNSError()) - } - .execute(completion) - } - } -} diff --git a/Modules/Sources/WordPressKit/KeyringConnection.swift b/Modules/Sources/WordPressKit/KeyringConnection.swift deleted file mode 100644 index 22656b33fdb4..000000000000 --- a/Modules/Sources/WordPressKit/KeyringConnection.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// KeyringConnection represents a keyring connected to a particular external service. -/// We only rarely need keyring data and we don't really need to persist it. For these -/// reasons KeyringConnection is treated like a model, even though it is not an NSManagedObject, -/// but also treated like it is a Remote Object. -/// -open class KeyringConnection: NSObject { - @objc open var additionalExternalUsers = [KeyringConnectionExternalUser]() - @objc open var dateIssued = Date() - @objc open var dateExpires: Date? - @objc open var externalID = "" // Some services uses strings for their IDs - @objc open var externalName = "" - @objc open var externalDisplay = "" - @objc open var externalProfilePicture = "" - @objc open var label = "" - @objc open var keyringID: NSNumber = 0 - @objc open var refreshURL = "" - @objc open var service = "" - @objc open var status = "" - @objc open var type = "" - @objc open var userID: NSNumber = 0 -} diff --git a/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift b/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift deleted file mode 100644 index 72a10467acbf..000000000000 --- a/Modules/Sources/WordPressKit/KeyringConnectionExternalUser.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation - -/// KeyringConnectionExternalUser represents an additional user account on the -/// external service that could be used other than the default account. -/// -open class KeyringConnectionExternalUser: NSObject { - @objc open var externalID = "" - @objc open var externalName = "" - @objc open var externalCategory = "" - @objc open var externalProfilePicture = "" -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift b/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift deleted file mode 100644 index 76b49f5c75e7..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeConnection.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -@objc open class RemotePublicizeConnection: NSObject { - @objc open var connectionID: NSNumber = 0 - @objc open var dateIssued = Date() - @objc open var dateExpires: Date? - @objc open var externalID = "" - @objc open var externalName = "" - @objc open var externalDisplay = "" - @objc open var externalProfilePicture = "" - @objc open var externalProfileURL = "" - @objc open var externalFollowerCount: NSNumber = 0 - @objc open var keyringConnectionID: NSNumber = 0 - @objc open var keyringConnectionUserID: NSNumber = 0 - @objc open var label = "" - @objc open var refreshURL = "" - @objc open var service = "" - @objc open var shared = false - @objc open var status = "" - @objc open var siteID: NSNumber = 0 - @objc open var userID: NSNumber = 0 -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift b/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift deleted file mode 100644 index 032fe371d042..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeInfo.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -public struct RemotePublicizeInfo: Decodable { - public let shareLimit: Int - public let toBePublicizedCount: Int - public let sharedPostsCount: Int - public let sharesRemaining: Int - - private enum CodingKeys: CodingKey { - case shareLimit - case toBePublicizedCount - case sharedPostsCount - case sharesRemaining - } -} diff --git a/Modules/Sources/WordPressKit/RemotePublicizeService.swift b/Modules/Sources/WordPressKit/RemotePublicizeService.swift deleted file mode 100644 index 2c0515642ec4..000000000000 --- a/Modules/Sources/WordPressKit/RemotePublicizeService.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -@objc open class RemotePublicizeService: NSObject { - @objc open var connectURL = "" - @objc open var detail = "" - @objc open var externalUsersOnly = false - @objc open var icon = "" - @objc open var jetpackSupport = false - @objc open var jetpackModuleRequired = "" - @objc open var label = "" - @objc open var multipleExternalUserIDSupport = false - @objc open var order: NSNumber = 0 - @objc open var serviceID = "" - @objc open var type = "" - @objc open var status = "" -} diff --git a/Modules/Sources/WordPressKit/SharingServiceRemote.swift b/Modules/Sources/WordPressKit/SharingServiceRemote.swift index ef99e0392829..52b6ac3fa3d5 100644 --- a/Modules/Sources/WordPressKit/SharingServiceRemote.swift +++ b/Modules/Sources/WordPressKit/SharingServiceRemote.swift @@ -2,7 +2,7 @@ import Foundation import NSObject_SafeExpectations /// SharingServiceRemote is responsible for wrangling the REST API calls related to -/// publiczice services, publicize connections, and keyring connections. +/// sharing buttons. /// open class SharingServiceRemote: ServiceRemoteWordPressComREST { @@ -30,425 +30,6 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { return NSError(domain: domain, code: code, userInfo: userInfo) } - // MARK: - Publicize Related Methods - - /// Fetches the list of Publicize services. - /// - /// - Parameters: - /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getPublicizeServices( - _ success: (([RemotePublicizeService]) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let endpoint = "meta/external-services" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let params = ["type": "publicize"] - - wordPressComRESTAPI.get(path, parameters: params as [String: AnyObject]?) { responseObject, httpResponse in - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - success?(self.remotePublicizeServicesFromDictionary(responseDict)) - } failure: { error, _ in - failure?(error as NSError) - } - } - - /// Fetches the list of Publicize services for a specified siteID. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting an array of `RemotePublicizeService` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - @objc open func getPublicizeServices( - for siteID: NSNumber, - success: (([RemotePublicizeService]) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let path = path(forEndpoint: "sites/\(siteID)/external-services", withVersion: ._2_0) - let params = ["type": "publicize" as AnyObject] - - wordPressComRESTAPI.get( - path, - parameters: params, - success: { response, httpResponse in - guard let responseDict = response as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - success?(self.remotePublicizeServicesFromDictionary(responseDict)) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Fetches the current user's list of keyring connections. - /// - /// - Parameters: - /// - success: An optional success block accepting an array of `KeyringConnection` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getKeyringConnections(_ success: (([KeyringConnection]) -> Void)?, failure: ((NSError?) -> Void)?) { - let endpoint = "me/keyring-connections" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.get( - path, - parameters: nil, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - let connections = responseDict.array(forKey: ConnectionDictionaryKeys.connections) ?? [] - let keyringConnections: [KeyringConnection] = connections.map { dict -> KeyringConnection in - let conn = KeyringConnection() - let dict = dict as AnyObject - let externalUsers = dict.array(forKey: ConnectionDictionaryKeys.additionalExternalUsers) ?? [] - conn.additionalExternalUsers = self.externalUsersForKeyringConnection(externalUsers as NSArray) - conn.dateExpires = WPKitDateUtils.date( - fromISOString: dict.string(forKey: ConnectionDictionaryKeys.expires) - ) - conn.dateIssued = WPKitDateUtils.date( - fromISOString: dict.string(forKey: ConnectionDictionaryKeys.issued) - ) - conn.externalDisplay = - dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay - conn.externalID = dict.string(forKey: ConnectionDictionaryKeys.externalID) ?? conn.externalID - conn.externalName = dict.string(forKey: ConnectionDictionaryKeys.externalName) ?? conn.externalName - if conn.externalDisplay.isEmpty { - conn.externalDisplay = conn.externalName - } - conn.externalProfilePicture = - dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) - ?? conn.externalProfilePicture - conn.keyringID = dict.number(forKey: ConnectionDictionaryKeys.ID) ?? conn.keyringID - conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label - conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL - conn.status = dict.string(forKey: ConnectionDictionaryKeys.status) ?? conn.status - conn.service = dict.string(forKey: ConnectionDictionaryKeys.service) ?? conn.service - conn.type = dict.string(forKey: ConnectionDictionaryKeys.type) ?? conn.type - conn.userID = dict.number(forKey: ConnectionDictionaryKeys.userID) ?? conn.userID - - return conn - } - - onSuccess(keyringConnections) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Creates KeyringConnectionExternalUser instances from the past array of - /// external user dictionaries. - /// - /// - Parameters: - /// - externalUsers: An array of NSDictionaries where each NSDictionary represents a KeyringConnectionExternalUser - /// - /// - Returns: An array of KeyringConnectionExternalUser instances. - /// - private func externalUsersForKeyringConnection(_ externalUsers: NSArray) -> [KeyringConnectionExternalUser] { - let arr: [KeyringConnectionExternalUser] = externalUsers.map { dict -> KeyringConnectionExternalUser in - let externalUser = KeyringConnectionExternalUser() - externalUser.externalID = - (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalID) ?? externalUser.externalID - externalUser.externalName = - (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalName) ?? externalUser.externalName - externalUser.externalProfilePicture = - (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalProfilePicture) - ?? externalUser.externalProfilePicture - externalUser.externalCategory = - (dict as AnyObject).string(forKey: ConnectionDictionaryKeys.externalCategory) - ?? externalUser.externalCategory - - return externalUser - } - return arr - } - - /// Fetches the current user's list of Publicize connections for the specified site's ID. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting an array of `RemotePublicizeConnection` objects. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func getPublicizeConnections( - _ siteID: NSNumber, - success: (([RemotePublicizeConnection]) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let endpoint = "sites/\(siteID)/publicize-connections" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.get( - path, - parameters: nil, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - let connections = responseDict.array(forKey: ConnectionDictionaryKeys.connections) ?? [] - let publicizeConnections: [RemotePublicizeConnection] = connections.compactMap { - dict -> RemotePublicizeConnection? in - let conn = self.remotePublicizeConnectionFromDictionary(dict as! NSDictionary) - return conn - } - - onSuccess(publicizeConnections) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Create a new Publicize connection bweteen the specified blog and - /// the third-pary service represented by the keyring. - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - keyringConnectionID: The ID of the third-party site's keyring connection. - /// - success: An optional success block accepting a `RemotePublicizeConnection` object. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func createPublicizeConnection( - _ siteID: NSNumber, - keyringConnectionID: NSNumber, - externalUserID: String?, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError) -> Void)? - ) { - - let endpoint = "sites/\(siteID)/publicize-connections/new" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - var parameters: [String: AnyObject] = [PublicizeConnectionParams.keyringConnectionID: keyringConnectionID] - if let userID = externalUserID { - parameters[PublicizeConnectionParams.externalUserID] = userID as AnyObject? - } - - wordPressComRESTAPI.post( - path, - parameters: parameters, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) - else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Update the shared status of the specified publicize connection - /// - /// - Parameters: - /// - connectionID: The ID of the publicize connection. - /// - externalID: The connection's externalID. Pass `nil` if the keyring - /// connection's default external ID should be used. Otherwise pass the external - /// ID of one if the keyring connection's `additionalExternalUsers`. - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func updatePublicizeConnectionWithID( - _ connectionID: NSNumber, - externalID: String?, - forSite siteID: NSNumber, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let externalUserID = (externalID == nil) ? "false" : externalID! - - let parameters = [ - PublicizeConnectionParams.externalUserID: externalUserID - ] - - wordPressComRESTAPI.post( - path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) - else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Update the shared status of the specified publicize connection - /// - /// - Parameters: - /// - connectionID: The ID of the publicize connection. - /// - shared: True if the connection is shared with all users of the blog. False otherwise. - /// - siteID: The WordPress.com ID of the site. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func updatePublicizeConnectionWithID( - _ connectionID: NSNumber, - shared: Bool, - forSite siteID: NSNumber, - success: ((RemotePublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - let parameters = [ - PublicizeConnectionParams.shared: shared - ] - - wordPressComRESTAPI.post( - path, - parameters: parameters as [String: AnyObject]?, - success: { responseObject, httpResponse in - guard let onSuccess = success else { - return - } - - guard let responseDict = responseObject as? NSDictionary, - let conn = self.remotePublicizeConnectionFromDictionary(responseDict) - else { - failure?(self.errorForUnexpectedResponse(httpResponse)) - return - } - - onSuccess(conn) - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Disconnects (deletes) the specified publicize connection - /// - /// - Parameters: - /// - siteID: The WordPress.com ID of the site. - /// - connectionID: The ID of the publicize connection. - /// - success: An optional success block accepting no arguments. - /// - failure: An optional failure block accepting an `NSError` argument. - /// - @objc open func deletePublicizeConnection( - _ siteID: NSNumber, - connectionID: NSNumber, - success: (() -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let endpoint = "sites/\(siteID)/publicize-connections/\(connectionID)/delete" - let path = self.path(forEndpoint: endpoint, withVersion: ._1_1) - - wordPressComRESTAPI.post( - path, - parameters: nil, - success: { _, _ in - success?() - }, - failure: { error, _ in - failure?(error as NSError) - } - ) - } - - /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` - /// - /// - Parameter dict: An `NSDictionary` representing a `RemotePublicizeConnection`. - /// - /// - Returns: A `RemotePublicizeConnection` object. - /// - private func remotePublicizeConnectionFromDictionary(_ dict: NSDictionary) -> RemotePublicizeConnection? { - guard let connectionID = dict.number(forKey: ConnectionDictionaryKeys.ID) else { - return nil - } - - let conn = RemotePublicizeConnection() - conn.connectionID = connectionID - conn.externalDisplay = dict.string(forKey: ConnectionDictionaryKeys.externalDisplay) ?? conn.externalDisplay - conn.externalID = dict.string(forKey: ConnectionDictionaryKeys.externalID) ?? conn.externalID - conn.externalName = dict.string(forKey: ConnectionDictionaryKeys.externalName) ?? conn.externalName - if conn.externalDisplay.isEmpty { - conn.externalDisplay = conn.externalName - } - conn.externalProfilePicture = - dict.string(forKey: ConnectionDictionaryKeys.externalProfilePicture) ?? conn.externalProfilePicture - conn.externalProfileURL = - dict.string(forKey: ConnectionDictionaryKeys.externalProfileURL) ?? conn.externalProfileURL - conn.keyringConnectionID = - dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionID) ?? conn.keyringConnectionID - conn.keyringConnectionUserID = - dict.number(forKey: ConnectionDictionaryKeys.keyringConnectionUserID) ?? conn.keyringConnectionUserID - conn.label = dict.string(forKey: ConnectionDictionaryKeys.label) ?? conn.label - conn.refreshURL = dict.string(forKey: ConnectionDictionaryKeys.refreshURL) ?? conn.refreshURL - conn.status = dict.string(forKey: ConnectionDictionaryKeys.status) ?? conn.status - conn.service = dict.string(forKey: ConnectionDictionaryKeys.service) ?? conn.service - - if let expirationDateAsString = dict.string(forKey: ConnectionDictionaryKeys.expires) { - conn.dateExpires = WPKitDateUtils.date(fromISOString: expirationDateAsString) - } - - if let issueDateAsString = dict.string(forKey: ConnectionDictionaryKeys.issued) { - conn.dateIssued = WPKitDateUtils.date(fromISOString: issueDateAsString) - } - - if let sharedDictNumber = dict.number(forKey: ConnectionDictionaryKeys.shared) { - conn.shared = sharedDictNumber.boolValue - } - - if let siteIDDictNumber = dict.number(forKey: ConnectionDictionaryKeys.siteID) { - conn.siteID = siteIDDictNumber - } - - if let userIDDictNumber = dict.number(forKey: ConnectionDictionaryKeys.userID) { - conn.userID = userIDDictNumber - } - - return conn - } - // MARK: - Sharing Button Related Methods /// Fetches the list of sharing buttons for a blog. @@ -533,7 +114,7 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { ) } - /// Composees a `RemotePublicizeConnection` populated with values from the passed `NSDictionary` + /// Composes `RemoteSharingButton` objects from the passed `NSArray` of `NSDictionary`s. /// /// - Parameter buttons: An `NSArray` of `NSDictionary`s representing `RemoteSharingButton` objects. /// @@ -578,92 +159,6 @@ open class SharingServiceRemote: ServiceRemoteWordPressComREST { return dict }) } - - private func remotePublicizeServicesFromDictionary(_ dictionary: NSDictionary) -> [RemotePublicizeService] { - let responseString = dictionary.description as NSString - let services: NSDictionary = - (dictionary.forKey(ServiceDictionaryKeys.services) as? NSDictionary) ?? NSDictionary() - - return services.allKeys.map { key in - let dict = (services.forKey(key) as? NSDictionary) ?? NSDictionary() - let pub = RemotePublicizeService() - - pub.connectURL = dict.string(forKey: ServiceDictionaryKeys.connectURL) ?? "" - pub.detail = dict.string(forKey: ServiceDictionaryKeys.description) ?? "" - pub.externalUsersOnly = dict.number(forKey: ServiceDictionaryKeys.externalUsersOnly)?.boolValue ?? false - pub.icon = dict.string(forKey: ServiceDictionaryKeys.icon) ?? "" - pub.serviceID = dict.string(forKey: ServiceDictionaryKeys.ID) ?? "" - pub.jetpackModuleRequired = dict.string(forKey: ServiceDictionaryKeys.jetpackModuleRequired) ?? "" - pub.jetpackSupport = dict.number(forKey: ServiceDictionaryKeys.jetpackSupport)?.boolValue ?? false - pub.label = dict.string(forKey: ServiceDictionaryKeys.label) ?? "" - pub.multipleExternalUserIDSupport = - dict.number(forKey: ServiceDictionaryKeys.multipleExternalUserIDSupport)?.boolValue ?? false - pub.type = dict.string(forKey: ServiceDictionaryKeys.type) ?? "" - pub.status = dict.string(forKey: ServiceDictionaryKeys.status) ?? "" - - // We're not guarenteed to get the right order by inspecting the - // response dictionary's keys. Instead, we can check the index - // of each service in the response string. - pub.order = NSNumber(value: responseString.range(of: pub.serviceID).location) - - return pub - } - } -} - -// Keys for PublicizeService dictionaries -private struct ServiceDictionaryKeys { - static let connectURL = "connect_URL" - static let description = "description" - static let externalUsersOnly = "external_users_only" - static let ID = "ID" - static let icon = "icon" - static let jetpackModuleRequired = "jetpack_module_required" - static let jetpackSupport = "jetpack_support" - static let label = "label" - static let multipleExternalUserIDSupport = "multiple_external_user_ID_support" - static let services = "services" - static let type = "type" - static let status = "status" -} - -// Keys for both KeyringConnection and PublicizeConnection dictionaries -private struct ConnectionDictionaryKeys { - // shared keys - static let connections = "connections" - static let expires = "expires" - static let externalID = "external_ID" - static let externalName = "external_name" - static let externalDisplay = "external_display" - static let externalProfilePicture = "external_profile_picture" - static let issued = "issued" - static let ID = "ID" - static let label = "label" - static let refreshURL = "refresh_URL" - static let service = "service" - static let sites = "sites" - static let status = "status" - static let userID = "user_ID" - - // only KeyringConnections - static let additionalExternalUsers = "additional_external_users" - static let type = "type" - static let externalCategory = "external_category" - - // only PublicizeConnections - static let externalFollowerCount = "external_follower_count" - static let externalProfileURL = "external_profile_URL" - static let keyringConnectionID = "keyring_connection_ID" - static let keyringConnectionUserID = "keyring_connection_user_ID" - static let shared = "shared" - static let siteID = "site_ID" -} - -// Names of parameters passed when creating or updating a publicize connection -private struct PublicizeConnectionParams { - static let keyringConnectionID = "keyring_connection_ID" - static let externalUserID = "external_user_ID" - static let shared = "shared" } // Names of parameters used in SharingButton requests diff --git a/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift b/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift deleted file mode 100644 index 467c639ab857..000000000000 --- a/Tests/KeystoneTests/Tests/Models/Blog+PublicizeTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import XCTest - -@testable import WordPress -@testable import WordPressData - -final class Blog_PublicizeTests: CoreDataTestCase { - - func testAutoSharingInfoForDotComBlog() { - let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false) - - // all dotcom sites should have no sharing limitations. - XCTAssertFalse(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForDotComBlogWithStoredSharingLimit() { - // unlikely case, but let's test anyway. - let blog = makeBlog(hostedAtWPCom: true, activeSocialFeature: false, hasPreExistingData: true) - - XCTAssertFalse(blog.isSocialSharingLimited) - XCTAssertNil(blog.sharingLimit) - } - - func testAutoSharingInfoForSelfHostedBlog() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false) - - XCTAssertTrue(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForSelfHostedBlogWithStoredSharingLimit() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: false, hasPreExistingData: true) - - XCTAssertNotNil(blog.sharingLimit) - } - - func testAutoSharingInfoForSelfHostedBlogWithSocialFeature() { - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true) - - XCTAssertFalse(blog.isSocialSharingLimited) - } - - func testAutoSharingInfoForSelfHostedBlogWithSocialFeatureAndStoredSharingLimit() { - // Example: a free site purchased an individual Social Basic plan. In this case, there should still be a - // `PublicizeInfo` data stored in Core Data, but the blog might still have the social feature active. - let blog = makeBlog(hostedAtWPCom: false, activeSocialFeature: true, hasPreExistingData: true) - - // the sharing limit should be nil regardless of the existence of stored data. - XCTAssertNil(blog.sharingLimit) - } - - // MARK: - Helpers - - private func makeBlog(hostedAtWPCom: Bool, activeSocialFeature: Bool, hasPreExistingData: Bool = false) -> Blog { - let blog = BlogBuilder(mainContext) - .with(isHostedAtWPCom: hostedAtWPCom) - .with(planActiveFeatures: activeSocialFeature ? ["social-shares-1000"] : []) - .build() - - if hasPreExistingData { - let publicizeInfo = PublicizeInfo(context: mainContext) - publicizeInfo.shareLimit = 30 - publicizeInfo.sharesRemaining = 25 - blog.publicizeInfo = publicizeInfo - } - - return blog - } -} diff --git a/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift b/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift deleted file mode 100644 index 5252ddeb5a46..000000000000 --- a/Tests/KeystoneTests/Tests/Models/Post+JetpackSocialTests.swift +++ /dev/null @@ -1,285 +0,0 @@ -import XCTest - -@testable import WordPress -@testable import WordPressData - -class Post_JetpackSocialTests: CoreDataTestCase { - - private let keyringAndConnectionIDPairs = [ - (100, 200), - (101, 201), - (102, 202) - ] - - private lazy var connections: Set = { - let connectionsArray = keyringAndConnectionIDPairs.map { keyringID, connectionID in - let connection = PublicizeConnection(context: mainContext) - connection.keyringConnectionID = NSNumber(value: keyringID) - connection.connectionID = NSNumber(value: connectionID) - return connection - } - return Set(connectionsArray) - }() - - private lazy var blog: Blog = { - BlogBuilder(mainContext).with(connections: connections).build() - }() - - // MARK: - Checking for PublicizeConnection state - - func testCheckPublicizeConnectionHavingOnlyKeyringIDEntry() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled] - ]) - - // When - let result = post.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - XCTAssertTrue(result) - } - - func testCheckPublicizeConnectionHavingOnlyConnectionIDEntry() { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .disabled] - ]) - - // When - let result = post.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - XCTAssertTrue(result) - } - - func testCheckPublicizeConnectionHavingDifferentKeyringAndConnectionEntries() { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled], - connectionID: [.valueKey: .disabled] - ]) - let post2 = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled], - connectionID: [.valueKey: .enabled] - ]) - - // When - let result1 = post.publicizeConnectionDisabledForKeyringID(keyringID) - let result2 = post2.publicizeConnectionDisabledForKeyringID(keyringID) - - // Then - // if either one of the value is true, then the method should return true. See: pctCYC-XT-p2#comment-1000 - XCTAssertTrue(result1) - XCTAssertTrue(result2) - } - - // MARK: - Disabling connections - - func testDisableConnectionWithoutAnyEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost() - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(entry[.valueKey], .disabled) - } - - func testDisableConnectionWithPriorKeyringEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(entry[.valueKey], .disabled) - } - - func testDisableConnectionWithPriorKeyringAndConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .enabled], - connectionID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // both entries' values should be updated. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .disabled) - - let connectionEntry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(connectionEntry[.valueKey], .disabled) - } - - func testDisableConnectionHavingOnlyConnectionIDEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .enabled] - ]) - - // When - post.disablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // if the keyring entry doesn't exist, the dictionary key should be updated to the keyringID. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .disabled) - - // the connection entry should be deleted. - XCTAssertNil(post.disabledPublicizeConnections?[connectionID]) - } - - // MARK: - Enabling connections - - // Note: unlikely case since there must be an entry in the `disabledPublicizeConnections` for the switch to be on. - func testEnableConnectionWithoutAnyEntries() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost() - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // Calling the enable method should do nothing. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - } - - func testEnableConnectionWithLocalKeyringEntry() { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // if the entry hasn't been synced yet, the entry will be deleted since all connections are enabled by default. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - } - - func testEnableConnectionWithSyncedKeyringEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled, .idKey: "24"] // having an id means the entry exists on backend. - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .enabled) - } - - // both keyring entry and connection entry are synced - func testEnableConnectionWithPriorSyncedKeyringAndConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled, .idKey: "24"], // having an id means the entry exists on backend. - connectionID: [.valueKey: .disabled, .idKey: "26"] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // both entries should be updated. - let keyringEntry = try XCTUnwrap(post.disabledPublicizeConnections?[keyringID]) - XCTAssertEqual(keyringEntry[.valueKey], .enabled) - - let connectionEntry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(connectionEntry[.valueKey], .enabled) - } - - // the keyring entry is local, but the connection entry is synced. - func testEnableConnectionWithPriorLocalKeyringAndSyncedConnectionEntries() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let post = makePost(disabledConnections: [ - keyringID: [.valueKey: .disabled], - connectionID: [.valueKey: .disabled, .idKey: "26"] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - - // Then - // the local entry should be removed. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - - // but the synced entry should be updated. - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(entry[.valueKey], .enabled) - } - - func testEnableConnectionHavingOnlyConnectionEntry() throws { - // Given - let keyringID = NSNumber(value: 100) - let connectionID = NSNumber(value: 200) - let keyringID2 = NSNumber(value: 101) - let connectionID2 = NSNumber(value: 201) - let post = makePost(disabledConnections: [ - connectionID: [.valueKey: .disabled, .idKey: "26"], - connectionID2: [.valueKey: .disabled] - ]) - - // When - post.enablePublicizeConnectionWithKeyringID(keyringID) - post.enablePublicizeConnectionWithKeyringID(keyringID2) - - // Then - // there shouldn't be any entries keyed by the keyringIDs. - XCTAssertNil(post.disabledPublicizeConnections?[keyringID]) - XCTAssertNil(post.disabledPublicizeConnections?[keyringID2]) - - // the entry with connectionID should be updated. - let entry = try XCTUnwrap(post.disabledPublicizeConnections?[connectionID]) - XCTAssertEqual(entry[.valueKey], .enabled) - - // and if the entry isn't synced, it should be deleted. - XCTAssertNil(post.disabledPublicizeConnections?[connectionID2]) - } - - // MARK: - Helpers - - private func makePost(disabledConnections: [NSNumber: [String: String]] = [:]) -> Post { - PostBuilder(mainContext, blog: blog) - .with(disabledConnections: disabledConnections) - .build() - } -} - -private extension String { - static let idKey = Post.Constants.publicizeIdKey - static let valueKey = Post.Constants.publicizeValueKey - static let disabled = Post.Constants.publicizeDisabledValue - static let enabled = Post.Constants.publicizeEnabledValue -} diff --git a/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift b/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift deleted file mode 100644 index 6cc727597023..000000000000 --- a/Tests/KeystoneTests/Tests/Services/JetpackSocialServiceTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -import Foundation -import XCTest -import OHHTTPStubs -import OHHTTPStubsSwift - -@testable import WordPress -@testable import WordPressData - -class JetpackSocialServiceTests: CoreDataTestCase { - - private let timeout: TimeInterval = 1.0 - private let blogID = 1001 - - private var jetpackSocialPath: String { - "/wpcom/v2/sites/\(blogID)/jetpack-social" - } - - private lazy var service: JetpackSocialService = { - .init(coreDataStack: contextManager) - }() - - override func setUp() { - super.setUp() - - BlogBuilder(mainContext).with(blogID: blogID).build() - contextManager.saveContextAndWait(mainContext) - } - - override func tearDown() { - HTTPStubs.removeAllStubs() - super.tearDown() - } - - // MARK: syncSharingLimit - - // non-existing PublicizeInfo + some RemotePublicizeInfo -> insert - func testSyncSharingLimitWithNewPublicizeInfo() throws { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: ["share_limit": 30, - "to_be_publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14] as [String: Any], - statusCode: 200, - headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - XCTAssertNotNil(sharingLimit) - XCTAssertEqual(sharingLimit?.remaining, 14) - XCTAssertEqual(sharingLimit?.limit, 30) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // non-existing PublicizeInfo + nil RemotePublicizeInfo -> nothing changes - func testSyncSharingLimitWithNilPublicizeInfo() { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - XCTAssertNil(sharingLimit) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // pre-existing PublicizeInfo + some RemotePublicizeInfo -> update - func testSyncSharingLimitWithNewPublicizeInfoGivenPreExistingData() throws { - try addPreExistingPublicizeInfo() - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: ["share_limit": 30, - "to_be_publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14] as [String: Any], - statusCode: 200, - headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - // the sharing limit fields should be updated according to the newest data. - XCTAssertNotNil(sharingLimit) - XCTAssertEqual(sharingLimit?.remaining, 14) - XCTAssertEqual(sharingLimit?.limit, 30) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // pre-existing PublicizeInfo + nil RemotePublicizeInfo -> delete - func testSyncSharingLimitWithNilPublicizeInfoGivenPreExistingData() throws { - try addPreExistingPublicizeInfo() - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should succeed") - service.syncSharingLimit(for: blogID) { result in - guard case .success(let sharingLimit) = result else { - XCTFail("syncSharingLimit unexpectedly failed") - return expectation.fulfill() - } - - // the pre-existing sharing limit should've been deleted. - XCTAssertNil(sharingLimit) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // non-existing blog ID + some RemotePublicizeInfo - func testSyncSharingLimitWithNewPublicizeInfoGivenInvalidBlogID() { - let invalidBlogID = 1002 - stub(condition: isPath("/wpcom/v2/sites/\(invalidBlogID)/jetpack-social")) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should fail") - service.syncSharingLimit(for: invalidBlogID) { result in - guard case .failure(let error) = result, - case .blogNotFound(let id) = error as? JetpackSocialService.ServiceError else { - XCTFail("Expected JetpackSocialService.ServiceError to occur") - return expectation.fulfill() - } - - XCTAssertEqual(id, invalidBlogID) - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - func testSyncSharingLimitRemoteFetchFailure() { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let expectation = expectation(description: "syncSharingLimit should fail") - service.syncSharingLimit(for: blogID) { result in - guard case .failure = result else { - XCTFail("syncSharingLimit unexpectedly succeeded") - return expectation.fulfill() - } - - expectation.fulfill() - } - wait(for: [expectation], timeout: timeout) - } - - // MARK: syncSharingLimit Objective-C - - func testObjcSyncSharingLimitSuccess() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 200, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertTrue(syncSucceeded) - } - - func testObjcSyncSharingLimitNilIDFailure() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertFalse(syncSucceeded) - } - - func testObjcSyncSharingLimitRequestFailure() async { - stub(condition: isPath(jetpackSocialPath)) { _ in - HTTPStubsResponse(jsonObject: [String: Any](), statusCode: 500, headers: nil) - } - - let syncSucceeded = await withCheckedContinuation { continuation in - service.syncSharingLimit(dotComID: NSNumber(value: blogID)) { - continuation.resume(returning: true) - } failure: { _ in - continuation.resume(returning: false) - } - } - - XCTAssertFalse(syncSucceeded) - } -} - -// MARK: - Helpers - -private extension JetpackSocialServiceTests { - - func addPreExistingPublicizeInfo() throws { - let blog = try Blog.lookup(withID: blogID, in: mainContext) - let info = PublicizeInfo(context: mainContext) - info.sharesRemaining = 550 - info.shareLimit = 1000 - blog?.publicizeInfo = info - contextManager.saveContextAndWait(mainContext) - } -} diff --git a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift index d4c1991809da..9b9521533247 100644 --- a/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift +++ b/Tests/KeystoneTests/Tests/Services/PostCoordinatorTests.swift @@ -493,10 +493,6 @@ class PostCoordinatorTests: CoreDataTestCase { // WHEN try await coordinator.publish(post) - - // THEN - XCTAssertEqual(post.publicizeMessage, "message-a") - XCTAssertEqual(post.publicizeMessageID, "752") } // MARK: - Misc diff --git a/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift b/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift deleted file mode 100644 index 8cbfa05b676e..000000000000 --- a/Tests/KeystoneTests/Tests/Services/PostHelperJetpackSocialTests.swift +++ /dev/null @@ -1,204 +0,0 @@ -import XCTest - -@testable import WordPress -@testable import WordPressData - -class PostHelperJetpackSocialTests: CoreDataTestCase { - let connectionId = 123 - let keyringId = 456 - - // MARK: - RemotePost -> Post tests - - func testMetadataWithConnectionIDKey() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_publicize_123", - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the keyring ID should be used as the key, while keeping the entry intact. - XCTAssertEqual(result[NSNumber(value: keyringId)], metadataEntry) - } - - func testMetadataWithKeyringIDKey() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_456", - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the keyring ID should be used as the key, while keeping the entry intact. - XCTAssertEqual(result[NSNumber(value: keyringId)], metadataEntry) - } - - func testMetadataWithConnectionIDKeyNotMatchingAnyPublicizeConnections() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: String] = [ - "id": "10", - "key": "_wpas_skip_publicize_500", // connectionId different from the one in local. - "value": "1" - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // the connection ID should be used as key. - XCTAssertEqual(result[NSNumber(value: 500)], metadataEntry) - } - - func testMetadataEntriesWithNonStringValues() { - // Given - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog) - let metadataEntry: [String: Any] = [ - "id": "10", - "key": "sharing_disabled", - "value": [123] - ] - - // When - let result = PostHelper.disabledPublicizeConnections(for: post, metadata: [metadataEntry]) - - // Then - // invalid entries should be ignored. - XCTAssertTrue(result.isEmpty) - } - - // MARK: - Post -> RemotePost tests - - func testDisabledConnectionsWithId() throws { - // Given - let connectionId2 = 234 - let keyringId2 = 567 - let presetDisabledConnections: [NSNumber: [String: String]] = [ - NSNumber(value: keyringId): ["id": "10", "value": "1"], - NSNumber(value: keyringId2): ["id": "11", "key": "_wpas_skip_\(keyringId2)", "value": "0"] - ] - let connections = makeConnections(with: [(connectionId, keyringId), (connectionId2, keyringId2)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 2) - - // keyless entries with id should default to the _wpas_skip_ format, despite having a matching connection. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId)" })) - - // entries with keys should be passed as is. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId2)" })) - } - - // should convert to publicize format. - func testKeylessDisabledConnectionsWithoutId() throws { - // Given - let connectionId2 = 234 - let keyringId2 = 567 - let presetDisabledConnections: [NSNumber: [String: String]] = [ - NSNumber(value: keyringId): ["value": "1"], - NSNumber(value: keyringId2): ["key": "_wpas_skip_\(keyringId2)", "value": "0"] - ] - let connections = makeConnections(with: [(connectionId, keyringId), (connectionId2, keyringId2)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 2) - - // local entries should be updated to the _wpas_skip_publicize format. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_publicize_\(connectionId)" })) - - // it's unlikely for a no-id entry to have a key, since it's assigned by the PostService when syncing. - // but in this unlikely case, the key should be sent as is. - let _ = try XCTUnwrap(entries.first(where: { $0["key"] == "_wpas_skip_\(keyringId2)" })) - } - - func testDisabledConnectionsWithoutIdAndNoMatchingPublicizeConnection() { - let nonExistentKeyringId = 3942 - let presetDisabledConnections = [NSNumber(value: nonExistentKeyringId): ["value": "1"]] - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - XCTAssertEqual(entries.count, 1) - - // local entries with no matching PublicizeConnection should default to _wpas_skip format. - XCTAssertEqual(entries.first?["key"], "_wpas_skip_\(nonExistentKeyringId)") - } - - func testInvalidDisabledConnectionEntry() { - let presetDisabledConnections = [NSNumber(value: 0): ["value": "1"]] - let connections = makeConnections(with: [(connectionId, keyringId)]) - let blog = makeBlog(connections: connections) - let post = makePost(for: blog, disabledConnections: presetDisabledConnections) - - // When - let entries = PostHelper.publicizeMetadataEntries(for: post) - - // Then - // Dictionary entries keyed by 0 should be ignored. This is a bug from previous implementation. - XCTAssertEqual(entries.count, 0) - } - - // MARK: - Test Helpers - - private func makeConnections(with pairs: [(connectionID: Int, keyringID: Int)]) -> Set { - return Set(pairs.map { - let connection = PublicizeConnection(context: mainContext) - connection.connectionID = NSNumber(value: $0.connectionID) - connection.keyringConnectionID = NSNumber(value: $0.keyringID) - - return connection - }) - } - - private func makeBlog(connections: Set? = nil) -> Blog { - var builder = BlogBuilder(mainContext) - - if let connections { - builder = builder.with(connections: connections) - } - - return builder.build() - } - - private func makePost(for blog: Blog, disabledConnections: [NSNumber: [String: String]]? = nil) -> Post { - var builder = PostBuilder(mainContext, blog: blog) - - if let disabledConnections { - builder = builder.with(disabledConnections: disabledConnections) - } - - return builder.build() - } -} diff --git a/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift b/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift deleted file mode 100644 index 83e75b8b7c74..000000000000 --- a/Tests/KeystoneTests/Tests/Services/SharingServiceTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import XCTest -import WordPressKit -import OHHTTPStubs -import OHHTTPStubsSwift - -@testable import WordPress -@testable import WordPressData - -class SharingServiceTests: CoreDataTestCase { - - private let userID = 101 - private let blogID = 10 - - private lazy var account: WPAccount = { - AccountBuilder(contextManager.mainContext) - .with(id: Int64(userID)) - .with(username: "username") - .with(authToken: "authToken") - .build() - }() - - private lazy var blog: Blog = { - let blog = BlogBuilder(mainContext).with(blogID: blogID).build() - blog.account = account - - // ensure that the changes are persisted to the stack. - contextManager.saveContextAndWait(mainContext) - return blog - }() - - // MARK: Sync Publicize Connections - - func testSyncingPublicizeConnectionsForNonDotComBlogCallsACompletionBlock() throws { - let blog = Blog.createBlankBlog(in: mainContext) - blog.account = nil - - let expect = expectation(description: "Sharing service completion block called.") - - let sharingService = SharingSyncService(coreDataStack: contextManager) - sharingService.syncPublicizeConnectionsForBlog(blog) { - expect.fulfill() - } failure: { _ in - expect.fulfill() - } - - waitForExpectations(timeout: 1, handler: nil) - } - - func testSyncingPublicizeConnectionsExcludesUnsharedConnections() async throws { - // Given - let service = SharingSyncService(coreDataStack: contextManager) - - stub(condition: isPath("/rest/v1.1/sites/\(blogID)/publicize-connections") && isMethodGET()) { _ in - HTTPStubsResponse(jsonObject: [ - "connections": [ - ["ID": 1000, "shared": "0", "user_ID": 101] as [String: Any], // owned connection - ["ID": 1001, "shared": "1", "user_ID": 201] as [String: Any], // shared connection - ["ID": 1002, "shared": "0", "user_ID": 301] as [String: Any], // private connection from others - ] - ], statusCode: 200, headers: nil) - } - - // When - try await withCheckedThrowingContinuation { continuation in - service.syncPublicizeConnectionsForBlog(blog) { - continuation.resume() - } failure: { error in - continuation.resume(throwing: error!) - } - } - - // Then - let connections = try XCTUnwrap(blog.connections) - - // the one with ID `1002` should be skipped since it's an unshared private connection from another user. - XCTAssertEqual(connections.count, 2) - - // connections owned by the user should be available. - XCTAssertTrue(connections.contains(where: { $0.connectionID.isEqual(to: NSNumber(value: 1000)) })) - - // shared connections owned by others should also be available. - XCTAssertTrue(connections.contains(where: { $0.connectionID.isEqual(to: NSNumber(value: 1001)) })) - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json deleted file mode 100644 index cee274bdc55f..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-403.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "code": "rest_cannot_view", - "message": "Required headers are missing from the request.", - "data": { - "status": 403 - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json deleted file mode 100644 index 3f84a1ccad67..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-no-publicize.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "is_share_limit_enabled": false, - "is_enhanced_publishing_enabled": false, - "is_social_image_generator_enabled": false -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json deleted file mode 100644 index e17256d046c7..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/jetpack-social-with-publicize.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "is_share_limit_enabled": true, - "to_be_publicized_count": 1, - "share_limit": 30, - "publicized_count": 15, - "shared_posts_count": 15, - "shares_remaining": 14, - "is_enhanced_publishing_enabled": false, - "is_social_image_generator_enabled": false -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json b/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json deleted file mode 100644 index c1798cdf0e43..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Mock Data/sites-external-services.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "services": { - "facebook": { - "ID": "facebook", - "label": "Facebook", - "type": "publicize", - "description": "Publish your posts to your Facebook timeline or page.", - "genericon": { - "class": "facebook-alt", - "unicode": "\\f203" - }, - "icon": "http:\/\/i.wordpress.com\/wp-content\/admin-plugins\/publicize\/assets\/publicize-fb-2x.png", - "connect_URL": "https:\/\/public-api.wordpress.com\/connect\/?action=request&kr_nonce=9b9bc62d36&nonce=a14cc808a1&for=connect&service=facebook&blog=193189927&kr_blog_nonce=e12bbe845e&magic=keyring", - "multiple_external_user_ID_support": true, - "external_users_only": true, - "jetpack_support": true, - "jetpack_module_required": "publicize" - }, - "twitter": { - "ID": "twitter", - "label": "Twitter", - "type": "publicize", - "description": "Publish your posts to your Twitter account.", - "genericon": { - "class": "twitter", - "unicode": "\\f202" - }, - "icon": "http:\/\/i.wordpress.com\/wp-content\/admin-plugins\/publicize\/assets\/publicize-twitter-2x.png", - "connect_URL": "https:\/\/public-api.wordpress.com\/connect\/?action=request&kr_nonce=9b9bc62d36&nonce=7aa41a23e8&for=connect&service=twitter&blog=193189927&kr_blog_nonce=e12bbe845e&magic=keyring", - "multiple_external_user_ID_support": false, - "external_users_only": false, - "jetpack_support": true, - "jetpack_module_required": "publicize", - "status": "unsupported" - } - } -} diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift index 2b2199ad280f..3bc1ef010461 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/SharingServiceRemoteTests.swift @@ -14,99 +14,8 @@ final class SharingServiceRemoteTests: RemoteTestCase, RESTTestable { SharingServiceRemote(wordPressComRestApi: api) }() - private let publicizeServicesMockFilename = "sites-external-services.json" - // MARK: - Tests - func testGetPublicizeServicesV1_1() { - let url = service.path(forEndpoint: "meta/external-services", withVersion: ._1_1) - - service.getPublicizeServices(nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - - let typeParameter = (api.parametersPassedIn as? NSDictionary)?.value(forKey: "type") as? String - XCTAssertEqual(typeParameter, "publicize", "Incorrect type parameter") - } - - func testGetPublicizeServicesV2() { - let mockID = NSNumber(value: 10) - let expectation = expectation(description: "Publicize services v2.0 should succeed") - let pathToStub = "sites/\(mockID)/external-services" - let mockService = SharingServiceRemote(wordPressComRestApi: getRestApi()) - - stubRemoteResponse(pathToStub, filename: publicizeServicesMockFilename, contentType: .ApplicationJSON) - - mockService.getPublicizeServices(for: mockID) { publicizeServices in - guard let facebookService = publicizeServices.first(where: { $0.serviceID == "facebook" }) else { - XCTFail("Expected a RemotePublicizeService to exist") - return - } - XCTAssertTrue(facebookService.status.isEmpty) - - guard let twitterService = publicizeServices.first(where: { $0.serviceID == "twitter" }) else { - XCTFail("Expected a RemotePublicizeService to exist") - return - } - XCTAssertEqual(twitterService.status, "unsupported") - - expectation.fulfill() - } failure: { _ in - XCTFail("Failure block unexpectedly called") - } - - wait(for: [expectation], timeout: timeout) - } - - func testGetKeyringServices() { - let url = service.path(forEndpoint: "me/keyring-connections", withVersion: ._1_1) - - service.getKeyringConnections(nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testGetPublicizeConnections() { - let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections", withVersion: ._1_1) - - service.getPublicizeConnections(mockID, success: nil, failure: nil) - - XCTAssertTrue(api.getMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testCreatePublicizeConnection() { - let mockID = NSNumber(value: 10) - let url = service.path(forEndpoint: "sites/\(mockID)/publicize-connections/new", withVersion: ._1_1) - - service.createPublicizeConnection( - mockID, - keyringConnectionID: mockID, - externalUserID: nil, - success: nil, - failure: nil - ) - - XCTAssertTrue(api.postMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - - func testDeletePublicizeConnection() { - let mockID = NSNumber(value: 10) - let url = service.path( - forEndpoint: "sites/\(mockID)/publicize-connections/\(mockID)/delete", - withVersion: ._1_1 - ) - - service.deletePublicizeConnection(mockID, connectionID: mockID, success: nil, failure: nil) - - XCTAssertTrue(api.postMethodCalled, "Method was not called") - XCTAssertEqual(api.URLStringPassedIn, url, "Incorrect URL passed in") - } - func testGetSharingButtonsForSite() { let mockID = NSNumber(value: 10) let url = service.path(forEndpoint: "sites/\(mockID)/sharing-buttons", withVersion: ._1_1) diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift deleted file mode 100644 index f015c1bfdda4..000000000000 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/Social/JetpackSocialServiceRemoteTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation -import XCTest -@testable import WordPressKit - -class JetpackSocialServiceRemoteTests: RemoteTestCase, RESTTestable { - let siteID = 1001 - let jetpackSocialWithPublicizeFilename = "jetpack-social-with-publicize.json" - let jetpackSocialWithoutPublicizeFilename = "jetpack-social-no-publicize.json" - let jetpackSocialErrorFilename = "jetpack-social-403.json" - - private var endpoint: String { - "sites/\(siteID)/jetpack-social" - } - - private lazy var remote: JetpackSocialServiceRemote = { - .init(wordPressComRestApi: getRestApi()) - }() - - // MARK: - Tests - - func testPublicizeInfoReturnValueForSitesWithPublicize() { - stubRemoteResponse(endpoint, filename: jetpackSocialWithPublicizeFilename, contentType: .ApplicationJSON) - let expect = expectation(description: "Jetpack Social request should succeed") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success(let info): - guard let info else { - XCTFail("PublicizeInfo should exist") - return - } - - // test parsing correctness - XCTAssertEqual(info.shareLimit, 30) - XCTAssertEqual(info.toBePublicizedCount, 1) - XCTAssertEqual(info.sharedPostsCount, 15) - XCTAssertEqual(info.sharesRemaining, 14) - expect.fulfill() - - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - wait(for: [expect], timeout: timeout) - } - - func testPublicizeInfoReturnValueForSitesWithoutPublicize() { - stubRemoteResponse(endpoint, filename: jetpackSocialWithoutPublicizeFilename, contentType: .ApplicationJSON) - let expect = expectation(description: "Jetpack Social request should succeed") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success(let info): - // for sites without publicize, the request should succeed with nil result. - XCTAssertNil(info) - expect.fulfill() - - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - } - wait(for: [expect], timeout: timeout) - } - - func testPublicizeInfoError() { - stubRemoteResponse(endpoint, filename: jetpackSocialErrorFilename, contentType: .ApplicationJSON, status: 403) - let expect = expectation(description: "Jetpack Social request should fail") - - remote.fetchPublicizeInfo(for: siteID) { result in - switch result { - case .success: - XCTFail("Unexpected success result") - case .failure: - expect.fulfill() - } - } - wait(for: [expect], timeout: timeout) - } -} diff --git a/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift b/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift deleted file mode 100644 index e5b46d8cf4ea..000000000000 --- a/WordPress/Classes/Models/Blog/Blog+JetpackSocial.swift +++ /dev/null @@ -1,31 +0,0 @@ -import WordPressData - -/// Blog extension for methods related to Jetpack Social. -extension Blog { - // MARK: - Publicize - - /// Whether the blog has Social auto-sharing limited. - /// Note that sites hosted at WP.com has no Social sharing limitations. - var isSocialSharingLimited: Bool { - let hasUnlimitedSharing = (planActiveFeatures ?? []).contains(Constants.unlimitedSharingFeatureKey) - return !(isHostedAtWPcom || isAtomic || hasUnlimitedSharing) - } - - /// The auto-sharing limit information for the blog. - var sharingLimit: PublicizeInfo.SharingLimit? { - // For blogs with unlimited shares, return nil early. - // This is because the endpoint will still return sharing limits as if the blog doesn't have unlimited sharing. - guard isSocialSharingLimited else { - return nil - } - return publicizeInfo?.sharingLimit - } - - // MARK: - Private constants - - private enum Constants { - /// The feature key listed in the blog's plan's features. At the moment, `social-shares-1000` means unlimited - /// sharing, but in the future we might introduce a proper differentiation between 1000 and unlimited. - static let unlimitedSharingFeatureKey = "social-shares-1000" - } -} diff --git a/WordPress/Classes/Models/PublicizeConnection+Creation.swift b/WordPress/Classes/Models/PublicizeConnection+Creation.swift deleted file mode 100644 index 7c65733ba0cc..000000000000 --- a/WordPress/Classes/Models/PublicizeConnection+Creation.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import WordPressData -import WordPressKit - -extension PublicizeConnection { - - /// Composes a new `PublicizeConnection`, or updates an existing one, with - /// data represented by the passed `RemotePublicizeConnection`. - /// - /// - Parameter remoteConnection: The remote connection representing the publicize connection. - /// - /// - Returns: A `PublicizeConnection`. - /// - static func createOrReplace(from remoteConnection: RemotePublicizeConnection, in context: NSManagedObjectContext) -> PublicizeConnection { - let pubConnection = findPublicizeConnection(byID: remoteConnection.connectionID, in: context) - ?? NSEntityDescription.insertNewObject(forEntityName: PublicizeConnection.entityName(), - into: context) as! PublicizeConnection - - pubConnection.connectionID = remoteConnection.connectionID - pubConnection.dateExpires = remoteConnection.dateExpires - pubConnection.dateIssued = remoteConnection.dateIssued - pubConnection.externalDisplay = remoteConnection.externalDisplay - pubConnection.externalFollowerCount = remoteConnection.externalFollowerCount - pubConnection.externalID = remoteConnection.externalID - pubConnection.externalName = remoteConnection.externalName - pubConnection.externalProfilePicture = remoteConnection.externalProfilePicture - pubConnection.externalProfileURL = remoteConnection.externalProfileURL - pubConnection.keyringConnectionID = remoteConnection.keyringConnectionID - pubConnection.keyringConnectionUserID = remoteConnection.keyringConnectionUserID - pubConnection.label = remoteConnection.label - pubConnection.refreshURL = remoteConnection.refreshURL - pubConnection.service = remoteConnection.service - pubConnection.shared = remoteConnection.shared - pubConnection.status = remoteConnection.status - pubConnection.siteID = remoteConnection.siteID - pubConnection.userID = remoteConnection.userID - - return pubConnection - } - - /// Finds a cached `PublicizeConnection` by its `connectionID` - /// - /// - Parameter connectionID: The ID of the `PublicizeConnection`. - /// - /// - Returns: The requested `PublicizeConnection` or nil. - /// - private static func findPublicizeConnection(byID connectionID: NSNumber, in context: NSManagedObjectContext) -> PublicizeConnection? { - let request = NSFetchRequest(entityName: PublicizeConnection.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "connectionID = %@", connectionID) - return try? context.fetch(request).first - } -} diff --git a/WordPress/Classes/Models/PublicizeService+Lookup.swift b/WordPress/Classes/Models/PublicizeService+Lookup.swift deleted file mode 100644 index fe29bada4c49..000000000000 --- a/WordPress/Classes/Models/PublicizeService+Lookup.swift +++ /dev/null @@ -1,48 +0,0 @@ -import CoreData -import WordPressData -import WordPressKit - -extension PublicizeService { - /// Finds a cached `PublicizeService` matching the specified service name. - /// - /// - Parameter name: The name of the service. This is the `serviceID` attribute for a `PublicizeService` object. - /// - /// - Returns: The requested `PublicizeService` or nil. - /// - static func lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) throws -> PublicizeService? { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "serviceID = %@", name) - return try context.fetch(request).first - } - - @objc(lookupPublicizeServiceNamed:inContext:) - public static func objc_lookupPublicizeServiceNamed(_ name: String, in context: NSManagedObjectContext) -> PublicizeService? { - try? lookupPublicizeServiceNamed(name, in: context) - } - - /// Returns an array of all cached `PublicizeService` objects. - /// - /// - Returns: An array of `PublicizeService`. The array is empty if no objects are cached. - /// - @objc(allPublicizeServicesInContext:error:) - public static func allPublicizeServices(in context: NSManagedObjectContext) throws -> [PublicizeService] { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) - request.sortDescriptors = [sortDescriptor] - return try context.fetch(request) - } - - /// Returns an array of all cached `PublicizeService` objects that are supported by Jetpack Social. - /// - /// Note that services without a `status` field from the remote will be marked as supported by default. - /// - /// - Parameter context: The managed object context. - /// - Returns: An array of `PublicizeService`. The array is empty if no objects are cached. - static func allSupportedServices(in context: NSManagedObjectContext) throws -> [PublicizeService] { - let request = NSFetchRequest(entityName: PublicizeService.classNameWithoutNamespaces()) - let sortDescriptor = NSSortDescriptor(key: "order", ascending: true) - request.predicate = NSPredicate(format: "status == %@", Self.defaultStatus) - request.sortDescriptors = [sortDescriptor] - return try context.fetch(request) - } -} diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m index 9df30485a4e6..5b471d2a223b 100644 --- a/WordPress/Classes/Services/BlogService.m +++ b/WordPress/Classes/Services/BlogService.m @@ -131,37 +131,6 @@ - (void)syncBlogAndAllMetadata:(Blog *)blog completionHandler:(void (^)(void))co dispatch_group_leave(syncGroup); }]; - SharingSyncService *sharingService = [[SharingSyncService alloc] initWithCoreDataStack:self.coreDataStack]; - dispatch_group_enter(syncGroup); - [sharingService syncPublicizeConnectionsForBlog:blog - success:^{ - dispatch_group_leave(syncGroup); - } - failure:^(NSError *error) { - DDLogError(@"Failed syncing publicize connections for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - SharingService *publicizeService = [[SharingService alloc] initWithContextManager:[ContextManager sharedInstance]]; - dispatch_group_enter(syncGroup); - [publicizeService syncPublicizeServicesForBlog:blog success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize services for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - - if ([RemoteFeature enabled:RemoteFeatureFlagJetpackSocialImprovements] && blog.dotComID != nil) { - JetpackSocialService *jetpackSocialService = [[JetpackSocialService alloc] initWithContextManager:ContextManager.sharedInstance]; - dispatch_group_enter(syncGroup); - [jetpackSocialService syncSharingLimitWithDotComID:blog.dotComID success:^{ - dispatch_group_leave(syncGroup); - } failure:^(NSError * _Nullable error) { - DDLogError(@"Failed syncing publicize sharing limit for blog %@: %@", blog.url, error); - dispatch_group_leave(syncGroup); - }]; - } - dispatch_group_enter(syncGroup); [remote getAllAuthorsWithSuccess:^(NSArray *users) { [self updateMultiAuthor:users forBlog:blogObjectID completionHandler:^{ diff --git a/WordPress/Classes/Services/JetpackSocialService.swift b/WordPress/Classes/Services/JetpackSocialService.swift deleted file mode 100644 index 2594bfbfac97..000000000000 --- a/WordPress/Classes/Services/JetpackSocialService.swift +++ /dev/null @@ -1,111 +0,0 @@ -import CoreData -import WordPressData -import WordPressKit - -@objc public class JetpackSocialService: NSObject { - - // MARK: Properties - - private let coreDataStack: CoreDataStackSwift - - private lazy var remote: JetpackSocialServiceRemote = { - let api = coreDataStack.performQuery { context in - return WordPressComRestApi.defaultV2Api(in: context) - } - return .init(wordPressComRestApi: api) - }() - - // MARK: Methods - - /// Init method for Objective-C. - /// - @objc public init(contextManager: ContextManager) { - self.coreDataStack = contextManager - } - - init(coreDataStack: CoreDataStackSwift = ContextManager.shared) { - self.coreDataStack = coreDataStack - } - - /// Fetches and updates the Publicize information for the site associated with the `blogID`. - /// The method returns a value type that contains the remaining usage of Social auto-sharing and the maximum limit for the associated site. - /// - /// - Note: If the returned result is a success with nil sharing limit, it's likely that the blog is hosted on WP.com, and has no Social sharing limitations. - /// - /// Furthermore, even if the sharing limit exists, it may not be applicable for the blog since the user might have purchased a product that ignores this limitation. - /// - /// - Parameters: - /// - blogID: The ID of the blog. - /// - completion: Closure that's called after the sync process completes. - func syncSharingLimit(for blogID: Int, completion: @escaping (Result) -> Void) { - // allow `self` to be retained inside this closure so the completion block will always be executed. - remote.fetchPublicizeInfo(for: blogID) { result in - switch result { - case .success(let remotePublicizeInfo): - self.coreDataStack.performAndSave({ context -> PublicizeInfo.SharingLimit? in - guard let blog = try Blog.lookup(withID: blogID, in: context) else { - // unexpected to fall into this case, since the API should return an error response. - throw ServiceError.blogNotFound(id: blogID) - } - - if let remotePublicizeInfo, - let newOrExistingInfo = blog.publicizeInfo ?? PublicizeInfo.newObject(in: context) { - // add or update the publicizeInfo for the blog. - newOrExistingInfo.configure(with: remotePublicizeInfo) - blog.publicizeInfo = newOrExistingInfo - } else if let existingPublicizeInfo = blog.publicizeInfo { - // if the remote object is nil, delete the blog's publicizeInfo if it exists. - context.delete(existingPublicizeInfo) - blog.publicizeInfo = nil - } - - return blog.publicizeInfo?.sharingLimit - }, completion: { completion($0) }, on: .main) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - /// Sync method for Objective-C. - /// Fetches the latest state of the blog's Publicize auto-sharing limits and stores them locally. - /// - /// - Parameters: - /// - dotComID: The WP.com ID of the blog. - /// - success: Closure called when the sync process succeeds. - /// - failure: Closure called when the sync process fails. - @objc public func syncSharingLimit(dotComID: NSNumber?, - success: (() -> Void)?, - failure: ((NSError?) -> Void)?) { - guard let blogID = dotComID?.intValue else { - failure?(ServiceError.nilBlogID as NSError) - return - } - - syncSharingLimit(for: blogID, completion: { result in - switch result { - case .success: - success?() - case .failure(let error): - failure?(error as NSError) - } - }) - } - - // MARK: Errors - - enum ServiceError: LocalizedError { - case blogNotFound(id: Int) - case nilBlogID - - var errorDescription: String? { - switch self { - case .blogNotFound(let id): - return "Blog with id: \(id) was unexpectedly not found." - case .nilBlogID: - return "Blog ID is unexpectedly nil." - } - } - } -} diff --git a/WordPress/Classes/Services/PostHelper+JetpackSocial.swift b/WordPress/Classes/Services/PostHelper+JetpackSocial.swift deleted file mode 100644 index 659be73c2122..000000000000 --- a/WordPress/Classes/Services/PostHelper+JetpackSocial.swift +++ /dev/null @@ -1,121 +0,0 @@ -import WordPressData - -extension PostHelper { - typealias StringDictionary = [String: String] - typealias Keys = Post.Constants - typealias SkipPrefix = Post.PublicizeMetadataSkipPrefix - - /// Returns a dictionary format for the `Post`'s `disabledPublicizeConnection` property based on the given metadata. - /// - /// This will try to handle both Publicize skip key formats, `_wpas_skip_{keyringID}` and `_wpas_skip_publicize_{connectionID`. - /// - /// There's a possibility that the `keyringID` obtained from remote doesn't match with any of the `PublicizeConnection` - /// that's stored locally, perhaps due to the app being out of sync. In this case, we'll fall back to using the old format. - /// - /// - Parameters: - /// - post: The associated `Post` object. Optional because Obj-C shouldn't be trusted. - /// - metadata: The metadata dictionary for the post. Optional because Obj-C shouldn't be trusted. - /// - Returns: A dictionary for the `Post`'s `disabledPublicizeConnections` property. - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept to avoid a Core Data migration and for remaining legacy references. - @objc(disabledPublicizeConnectionsForPost:andMetadata:) - static func disabledPublicizeConnections( - for post: AbstractPost?, - metadata: [[String: Any]]? - ) -> [NSNumber: StringDictionary] { - guard let post, let metadata else { - return [:] - } - - return - metadata - .compactMap { $0 as? [String: String] } - .filter { $0[Keys.publicizeKeyKey]?.hasPrefix(SkipPrefix.keyring.rawValue) ?? false } - .reduce(into: [NSNumber: StringDictionary]()) { partialResult, entry in - // every metadata entry should have a key. - guard let key = entry[Keys.publicizeKeyKey] else { - return - } - - func getDictionaryID() -> Int? { - guard let prefixType = SkipPrefix.prefix(of: key) else { - return nil - } - - switch prefixType { - case .keyring: - return Int(key.removingPrefix(SkipPrefix.keyring.rawValue)) - - case .connection: - // If the key uses the new format, try to find an existing `PublicizeConnection` matching - // the connectionID, and return its keyringID. - let entryConnectionID = Int(key.removingPrefix(SkipPrefix.connection.rawValue)) - - guard let connections = post.blog.connections, - let connectionID = entryConnectionID, - let connection = connections.first(where: { $0.connectionID.intValue == connectionID }) - else { - /// Otherwise, fall back to the connectionID extracted from the metadata key. - /// Note that entries with `connectionID` won't be detected by the Post's - /// `publicizeConnectionDisabledForKeyringID` method. - /// - /// However, the Publicize methods in `Post.swift` will attempt to update the key into - /// its `keyringID`, since the `PublicizeConnection` object is guaranteed to exist. - return entryConnectionID - } - - return connection.keyringConnectionID.intValue - } - } - - if let id = getDictionaryID() { - partialResult[NSNumber(value: id)] = entry - } - } - } - - /// Converts the `Post`'s `disabledPublicizeConnections` dictionary to metadata entries. - /// - /// - Parameter post: The associated `Post` object. - /// - Returns: An array of metadata dictionaries representing the `Post`'s disabled connections. - // Deprecated: superseded for post editing by connection_id-keyed PostSocialSharingDraft stored in post metadata. - // Kept to avoid a Core Data migration and for remaining legacy references. - @objc(publicizeMetadataEntriesForPost:) - static func publicizeMetadataEntries(for post: Post?) -> [StringDictionary] { - guard let post, - let disabledConnectionsDictionary = post.disabledPublicizeConnections - else { - return [] - } - - return disabledConnectionsDictionary.compactMap { (keyringID: NSNumber, entry: StringDictionary) in - // The previous implementation didn't properly parse `_wpas_skip_publicize_` keys, causing it - // to use 0 as the dictionary key. Although this will be ignored by the server, let's make sure - // it's not sent to the remote any longer. - guard keyringID.intValue > 0 else { - return nil - } - - // Each entry should have a `value`, an optional `id`, and an optional `key`. - // If the entry already has a key, then there's nothing to do; Pass the dictionary as is. - if let _ = entry[Keys.publicizeKeyKey] { - return entry - } - - // If the key doesn't exist, this means that the dictionary is still using the old format. - // Try to add a key with the new format ONLY if the metadata hasn't been synced to the remote. - let metadataKeyValue: String = { - guard entry[Keys.publicizeIdKey] == nil, - let connections = post.blog.connections, - let connection = connections.first(where: { $0.keyringConnectionID == keyringID }) - else { - // Fall back to the old keyring format. - return "\(SkipPrefix.keyring.rawValue)\(keyringID)" - } - return "\(SkipPrefix.connection.rawValue)\(connection.connectionID)" - }() - - return entry.merging([Keys.publicizeKeyKey: metadataKeyValue]) { _, newValue in newValue } - } - } -} diff --git a/WordPress/Classes/Services/PostHelper.m b/WordPress/Classes/Services/PostHelper.m index 6e8d9fd0813a..976bb96e3ea2 100644 --- a/WordPress/Classes/Services/PostHelper.m +++ b/WordPress/Classes/Services/PostHelper.m @@ -89,8 +89,6 @@ + (void)updatePost:(AbstractPost *)post withRemotePost:(RemotePost *)remotePost [self updatePost:postPost withRemoteCategories:remotePost.categories inContext:managedObjectContext]; NSString *publicID = nil; - NSString *publicizeMessage = nil; - NSString *publicizeMessageID = nil; if (remotePost.metadata) { NSDictionary *latitudeDictionary = [self dictionaryWithKey:@"geo_latitude" inMetadata:remotePost.metadata]; NSDictionary *longitudeDictionary = [self dictionaryWithKey:@"geo_longitude" inMetadata:remotePost.metadata]; @@ -103,14 +101,8 @@ + (void)updatePost:(AbstractPost *)post withRemotePost:(RemotePost *)remotePost coord.longitude = [longitude doubleValue]; publicID = [geoPublicDictionary stringForKey:@"id"]; } - NSDictionary *publicizeMessageDictionary = [self dictionaryWithKey:@"_wpas_mess" inMetadata:remotePost.metadata]; - publicizeMessage = [publicizeMessageDictionary stringForKey:@"value"]; - publicizeMessageID = [publicizeMessageDictionary stringForKey:@"id"]; } postPost.publicID = publicID; - postPost.publicizeMessage = publicizeMessage; - postPost.publicizeMessageID = publicizeMessageID; - postPost.disabledPublicizeConnections = [self disabledPublicizeConnectionsForPost:post andMetadata:remotePost.metadata]; } } diff --git a/WordPress/Classes/Services/SharingService.swift b/WordPress/Classes/Services/SharingService.swift index 1d4885f1ce6a..5a59346c5700 100644 --- a/WordPress/Classes/Services/SharingService.swift +++ b/WordPress/Classes/Services/SharingService.swift @@ -1,10 +1,8 @@ import Foundation import WordPressData -import WordPressShared import WordPressKit -/// SharingService is responsible for wrangling publicize services, publicize -/// connections, and keyring connections. +/// SharingService is responsible for wrangling sharing buttons. /// @objc public class SharingService: NSObject { let SharingAPIErrorNotFound = "not_found" @@ -23,433 +21,6 @@ import WordPressKit self.coreDataStack = coreDataStack } - // MARK: - Publicize Related Methods - - /// Syncs the list of Publicize services. The list is expected to very rarely change. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync publicize services - /// - success: An optional success block accepting no parameters - /// - failure: An optional failure block accepting an `NSError` parameter - /// - @objc public func syncPublicizeServicesForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) - { - guard let remote = remoteForBlog(blog), - let blogID = blog.dotComID - else { - failure?(nil) - return - } - - remote.getPublicizeServices( - for: blogID, - success: { remoteServices in - // Process the results - self.mergePublicizeServices(remoteServices, success: success) - }, - failure: failure - ) - } - - /// Fetches the current user's list of keyring connections. Nothing is saved to core data. - /// The success block should accept an array of `KeyringConnection` objects. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync keyring connections - /// - success: An optional success block accepting an array of `KeyringConnection` objects - /// - failure: An optional failure block accepting an `NSError` parameter - /// - @objc public func fetchKeyringConnectionsForBlog( - _ blog: Blog, - success: (([KeyringConnection]) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - guard let remote = remoteForBlog(blog) else { - return - } - remote.getKeyringConnections( - { keyringConnections in - // Just return the result - success?(keyringConnections) - }, - failure: failure - ) - } - - /// Creates a new publicize connection for the specified `Blog`, using the specified - /// keyring. Optionally the connection can target a particular external user account. - /// - /// - Parameters - /// - blog: The `Blog` for which to sync publicize connections - /// - keyring: The `KeyringConnection` to use - /// - externalUserID: An optional string representing a user ID on the external service. - /// - success: An optional success block accepting a `PublicizeConnection` parameter. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func createPublicizeConnectionForBlog( - _ blog: Blog, - keyring: KeyringConnection, - externalUserID: String?, - success: ((PublicizeConnection) -> Void)?, - failure: ((NSError?) -> Void)? - ) { - let blogObjectID = blog.objectID - guard let remote = remoteForBlog(blog) else { - return - } - let dotComID = blog.dotComID! - remote.createPublicizeConnection( - dotComID, - keyringConnectionID: keyring.keyringID, - externalUserID: externalUserID, - success: { remoteConnection in - let properties = [ - "service": keyring.service - ] - WPAppAnalytics.track(.sharingPublicizeConnected, properties: properties, blogID: dotComID) - - self.coreDataStack.performAndSave( - { context -> NSManagedObjectID in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID( - blogObjectID, - remoteConnection: remoteConnection, - in: context - ) - }, - completion: { result in - let transformed = result.flatMap { objectID in - Result { - let object = try self.coreDataStack.mainContext.existingObject(with: objectID) - return object as! PublicizeConnection - } - } - switch transformed { - case let .success(object): - success?(object) - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, - on: .main - ) - }, - failure: { (error: NSError?) in - failure?(error) - } - ) - } - - /// Update the specified `PublicizeConnection`. The update to core data is performed - /// optimistically. In case of failure the original value will be restored. - /// - /// - Parameters: - /// - shared: True if the connection should be shared with all users of the blog. - /// - pubConn: The `PublicizeConnection` to update - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func updateSharedForBlog( - _ blog: Blog, - shared: Bool, - forPublicizeConnection pubConn: PublicizeConnection, - success: (() -> Void)?, - failure: ((NSError?) -> Void)? - ) { - typealias PubConnUpdateResult = ( - oldValue: Bool, siteID: NSNumber, connectionID: NSNumber, service: String, remote: SharingServiceRemote? - ) - - let blogObjectID = blog.objectID - coreDataStack.performAndSave( - { context -> PubConnUpdateResult in - let blogInContext = try context.existingObject(with: blogObjectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - let oldValue = pubConnInContext.shared - pubConnInContext.shared = shared - return ( - oldValue: oldValue, - siteID: pubConnInContext.siteID, - connectionID: pubConnInContext.connectionID, - service: pubConnInContext.service, - remote: self.remoteForBlog(blogInContext) - ) - }, - completion: { result in - switch result { - case let .success(value): - if value.oldValue == shared { - success?() - return - } - - value.remote? - .updatePublicizeConnectionWithID( - value.connectionID, - shared: shared, - forSite: value.siteID, - success: { remoteConnection in - let properties = [ - "service": value.service, - "is_site_wide": NSNumber(value: shared).stringValue - ] - WPAppAnalytics.track( - .sharingPublicizeConnectionAvailableToAllChanged, - properties: properties, - blogID: value.siteID - ) - - self.coreDataStack.performAndSave( - { context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID( - blogObjectID, - remoteConnection: remoteConnection, - in: context - ) - }, - completion: { result in - switch result { - case .success: - success?() - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, - on: .main - ) - }, - failure: { (error: NSError?) in - self.coreDataStack.performAndSave( - { context in - let pubConnInContext = - try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - pubConnInContext.shared = value.oldValue - }, - completion: { _ in - failure?(error) - }, - on: .main - ) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, - on: .main - ) - } - - /// Update the specified `PublicizeConnection`. The update to core data is performed - /// optimistically. In case of failure the original value will be restored. - /// - /// - Parameters: - /// - externalID: True if the connection should be shared with all users of the blog. - /// - pubConn: The `PublicizeConnection` to update - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func updateExternalID( - _ externalID: String, - forBlog blog: Blog, - forPublicizeConnection pubConn: PublicizeConnection, - success: (() -> Void)?, - failure: ((NSError?) -> Void)? - ) { - if pubConn.externalID == externalID { - success?() - return - } - - let blogObjectID = blog.objectID - let siteID = pubConn.siteID - guard let remote = remoteForBlog(blog) else { - return - } - remote.updatePublicizeConnectionWithID( - pubConn.connectionID, - externalID: externalID, - forSite: siteID, - success: { remoteConnection in - self.coreDataStack.performAndSave( - { context in - try self.createOrReplacePublicizeConnectionForBlogWithObjectID( - blogObjectID, - remoteConnection: remoteConnection, - in: context - ) - }, - completion: { result in - switch result { - case .success: - success?() - case let .failure(error): - DDLogError("Error creating publicize connection from remote: \(error)") - failure?(error as NSError) - } - }, - on: .main - ) - }, - failure: failure - ) - } - - /// Deletes the specified `PublicizeConnection`. The delete from core data is performed - /// optimistically. The caller's `failure` block should be responsible for resyncing - /// the deleted connection. - /// - /// - Parameters: - /// - pubConn: The `PublicizeConnection` to delete - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an NSError parameter. - /// - @objc public func deletePublicizeConnectionForBlog( - _ blog: Blog, - pubConn: PublicizeConnection, - success: (() -> Void)?, - failure: ((NSError?) -> Void)? - ) { - // optimistically delete the connection locally. - coreDataStack.performAndSave( - { context in - let blogInContext = try context.existingObject(with: blog.objectID) as! Blog - let pubConnInContext = try context.existingObject(with: pubConn.objectID) as! PublicizeConnection - - let siteID = pubConnInContext.siteID - context.delete(pubConnInContext) - return ( - siteID, pubConnInContext.connectionID, pubConnInContext.service, self.remoteForBlog(blogInContext) - ) - }, - completion: { result in - switch result { - case let .success((siteID, connectionID, service, remote)): - remote? - .deletePublicizeConnection( - siteID, - connectionID: connectionID, - success: { - let properties = [ - "service": service - ] - WPAppAnalytics.track( - .sharingPublicizeDisconnected, - properties: properties, - blogID: siteID - ) - success?() - }, - failure: { (error: NSError?) in - if let errorCode = error?.userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String { - if errorCode == self.SharingAPIErrorNotFound { - // This is a special situation. If the call to disconnect the service returns not_found then the service - // has probably already been disconnected and the call was made with stale data. - // Assume this is the case and treat this error as a successful disconnect. - success?() - return - } - } - failure?(error) - } - ) - case let .failure(error): - failure?(error as NSError) - } - }, - on: .main - ) - } - - // MARK: - Private PublicizeService Methods - - /// Called when syncing Publicize services. Merges synced and cached data, removing - /// anything that does not exist on the server. Saves the context. - /// - /// - Parameters - /// - remoteServices: An array of `RemotePublicizeService` objects to merge. - /// - success: An optional callback block to be performed when core data has saved the changes. - /// - private func mergePublicizeServices(_ remoteServices: [RemotePublicizeService], success: (() -> Void)?) { - coreDataStack.performAndSave( - { context in - let currentPublicizeServices = (try? PublicizeService.allPublicizeServices(in: context)) ?? [] - - // Create or update based on the contents synced. - let servicesToKeep = remoteServices.map { remoteService -> PublicizeService in - self.createOrReplaceFromRemotePublicizeService(remoteService, in: context) - } - - // Delete any cached PublicizeServices that were not synced. - for pubService in currentPublicizeServices { - if !servicesToKeep.contains(pubService) { - context.delete(pubService) - } - } - }, - completion: success, - on: .main - ) - } - - /// Composes a new `PublicizeService`, or updates an existing one, with data represented by the passed `RemotePublicizeService`. - /// - /// - Parameter remoteService: The remote publicize service representing a `PublicizeService` - /// - /// - Returns: A `PublicizeService`. - /// - private func createOrReplaceFromRemotePublicizeService( - _ remoteService: RemotePublicizeService, - in context: NSManagedObjectContext - ) -> PublicizeService { - var pubService = try? PublicizeService.lookupPublicizeServiceNamed(remoteService.serviceID, in: context) - if pubService == nil { - pubService = - NSEntityDescription.insertNewObject( - forEntityName: PublicizeService.entityName(), - into: context - ) as? PublicizeService - } - pubService?.connectURL = remoteService.connectURL - pubService?.detail = remoteService.detail - pubService?.externalUsersOnly = remoteService.externalUsersOnly - pubService?.icon = remoteService.icon - pubService?.jetpackModuleRequired = remoteService.jetpackModuleRequired - pubService?.jetpackSupport = remoteService.jetpackSupport - pubService?.label = remoteService.label - pubService?.multipleExternalUserIDSupport = remoteService.multipleExternalUserIDSupport - pubService?.order = remoteService.order - pubService?.serviceID = remoteService.serviceID - pubService?.type = remoteService.type - pubService?.status = (remoteService.status.isEmpty ? PublicizeService.defaultStatus : remoteService.status) - - return pubService! - } - - // MARK: - Private PublicizeConnection Methods - - /// Composes a new `PublicizeConnection`, with data represented by the passed `RemotePublicizeConnection`. - /// Throws an error if unable to find a `Blog` for the `blogObjectID` - /// - /// - Parameter blogObjectID: And `NSManagedObjectID` for for a `Blog` entity. - /// - /// - Returns: A `PublicizeConnection`. - /// - private func createOrReplacePublicizeConnectionForBlogWithObjectID( - _ blogObjectID: NSManagedObjectID, - remoteConnection: RemotePublicizeConnection, - in context: NSManagedObjectContext - ) throws -> NSManagedObjectID { - let blog = try context.existingObject(with: blogObjectID) as! Blog - let pubConn = PublicizeConnection.createOrReplace(from: remoteConnection, in: context) - pubConn.blog = blog - - try context.obtainPermanentIDs(for: [pubConn]) - - return pubConn.objectID - } - // MARK: Sharing Button Related Methods /// Syncs `SharingButton`s for the specified wpcom blog. diff --git a/WordPress/Classes/Services/SharingSyncService.swift b/WordPress/Classes/Services/SharingSyncService.swift deleted file mode 100644 index 603eb602de74..000000000000 --- a/WordPress/Classes/Services/SharingSyncService.swift +++ /dev/null @@ -1,124 +0,0 @@ -import Foundation -import WordPressData -import WordPressShared -import WordPressKit - -/// SharingService is responsible for wrangling publicize services, publicize -/// connections, and keyring connections. -/// -@objc public class SharingSyncService: NSObject { - - let coreDataStack: CoreDataStack - - @objc public init(coreDataStack: CoreDataStack) { - self.coreDataStack = coreDataStack - } - - /// Returns the remote to use with the service. - /// - /// - Parameter blog: The blog to use for the rest api. - /// - private func remoteForBlog(_ blog: Blog) -> SharingServiceRemote? { - guard let api = blog.wordPressComRestApi else { - return nil - } - - return SharingServiceRemote(wordPressComRestApi: api) - } - - /// Syncs Publicize connections for the specified wpcom blog. - /// - /// - Parameters: - /// - blog: The `Blog` for which to sync publicize connections - /// - success: An optional success block accepting no parameters. - /// - failure: An optional failure block accepting an `NSError` parameter. - /// - @objc open func syncPublicizeConnectionsForBlog(_ blog: Blog, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { - guard let remote = remoteForBlog(blog), - let blogID = blog.dotComID else { - failure?(Error.siteWithNoRemote as NSError) - return - } - - remote.getPublicizeConnections(blogID, success: { [blogObjectID = blog.objectID] remoteConnections in - let currentUserID: NSNumber = self.coreDataStack.performQuery { context in - guard let blog = try? context.existingObject(with: blogObjectID) as? Blog, - let accountID = blog.account?.userID else { - return NSNumber(value: 0) - } - return accountID - } - - // Ensure that we're only processing shared or owned connections - let authorizedConnections = remoteConnections.filter { $0.shared || $0.userID.isEqual(to: currentUserID) } - - // Process the results - self.mergePublicizeConnectionsForBlog(blogObjectID, remoteConnections: authorizedConnections, onComplete: success) - }, failure: failure) - } - - /// Called when syncing Publicize connections. Merges synced and cached data, removing - /// anything that does not exist on the server. Saves the context. - /// - /// - Parameters: - /// - blogObjectID: the NSManagedObjectID of a `Blog` - /// - remoteConnections: An array of `RemotePublicizeConnection` objects to merge. - /// - onComplete: An optional callback block to be performed when core data has saved the changes. - /// - private func mergePublicizeConnectionsForBlog(_ blogObjectID: NSManagedObjectID, remoteConnections: [RemotePublicizeConnection], onComplete: (() -> Void)?) { - coreDataStack.performAndSave({ context in - var blog: Blog - do { - blog = try context.existingObject(with: blogObjectID) as! Blog - } catch let error as NSError { - DDLogError("Error fetching Blog: \(error)") - // Because of the error we'll bail early, but we still need to call - // the success callback if one was passed. - return - } - - let currentPublicizeConnections = self.allPublicizeConnections(for: blog, in: context) - - // Create or update based on the contents synced. - let connectionsToKeep = remoteConnections.map { remoteConnection -> PublicizeConnection in - let pubConnection = PublicizeConnection.createOrReplace(from: remoteConnection, in: context) - pubConnection.blog = blog - return pubConnection - } - - // Delete any cached PublicizeServices that were not synced. - for pubConnection in currentPublicizeConnections { - if !connectionsToKeep.contains(pubConnection) { - context.delete(pubConnection) - } - } - }, completion: { onComplete?() }, on: .main) - } - - /// Returns an array of all cached `PublicizeConnection` objects. - /// - /// - Parameters - /// - blog: A `Blog` object - /// - /// - Returns: An array of `PublicizeConnection`. The array is empty if no objects are cached. - /// - private func allPublicizeConnections(for blog: Blog, in context: NSManagedObjectContext) -> [PublicizeConnection] { - let request = NSFetchRequest(entityName: PublicizeConnection.classNameWithoutNamespaces()) - request.predicate = NSPredicate(format: "blog = %@", blog) - - var connections: [PublicizeConnection] - do { - connections = try context.fetch(request) as! [PublicizeConnection] - } catch let error as NSError { - DDLogError("Error fetching Publicize Connections: \(error.localizedDescription)") - connections = [] - } - - return connections - } - - // Error for failure states - enum Error: Swift.Error { - case siteWithNoRemote - } -} diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 68d0e5c1431c..7618900e2fca 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -19,7 +19,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { case bloggingPromptsSocial case siteEditorMVP case contactSupportChatbot - case jetpackSocialImprovements case domainManagement case dynamicDashboardCards case plansInSiteCreation @@ -68,8 +67,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return true case .contactSupportChatbot: return false - case .jetpackSocialImprovements: - return AppConfiguration.isJetpack case .domainManagement: return false case .dynamicDashboardCards: @@ -130,8 +127,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "site_editor_mvp" case .contactSupportChatbot: return "contact_support_chatbot" - case .jetpackSocialImprovements: - return "jetpack_social_improvements_v1" case .domainManagement: return "domain_management" case .dynamicDashboardCards: @@ -191,8 +186,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "Site Editor MVP" case .contactSupportChatbot: return "Contact Support via DocsBot" - case .jetpackSocialImprovements: - return "Jetpack Social Improvements v1" case .domainManagement: return "Domain Management" case .dynamicDashboardCards: From 9d465e097b487800aba5274249cb289d56c0a145 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 16:12:21 +1200 Subject: [PATCH 7/9] Apply swift-format ahead of the legacy publicize model removal --- .../WordPressData/Blog+Extensions.swift | 3 ++- .../Helpers/BlogBuilder.swift | 25 ++++++++++------- .../Helpers/PostBuilder.swift | 8 +++--- Tests/KeystoneTests/Helpers/PostBuilder.swift | 8 +++--- .../Tests/Models/PostTests.swift | 27 ++++++++++++------- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Modules/Sources/WordPressData/Blog+Extensions.swift b/Modules/Sources/WordPressData/Blog+Extensions.swift index ffe6ed0b235a..060b24c1f759 100644 --- a/Modules/Sources/WordPressData/Blog+Extensions.swift +++ b/Modules/Sources/WordPressData/Blog+Extensions.swift @@ -29,7 +29,8 @@ extension Blog { } // Add remaining formats sorted by their display names - let nonStandardFormats = postFormats + let nonStandardFormats = + postFormats .filter { $0.key != Blog.postFormatStandard } .sorted { $0.value.localizedCaseInsensitiveCompare($1.value) == .orderedAscending } .map { $0.key } diff --git a/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift b/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift index ea4d377311ad..75829e8265bd 100644 --- a/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift +++ b/Modules/Tests/WordPressDataTests/Helpers/BlogBuilder.swift @@ -28,7 +28,7 @@ class BlogBuilder { } func with(atomic: Bool) -> Self { - return set(blogOption: "is_wpcom_atomic", value: atomic ? 1 : 0) + set(blogOption: "is_wpcom_atomic", value: atomic ? 1 : 0) } func with(isHostedAtWPCom: Bool) -> Self { @@ -37,7 +37,7 @@ class BlogBuilder { } func with(supportsDomains: Bool) -> Self { - return with(isHostedAtWPCom: supportsDomains) + with(isHostedAtWPCom: supportsDomains) .with(isAdmin: supportsDomains) } @@ -60,7 +60,7 @@ class BlogBuilder { } func with(wordPressVersion: String) -> Self { - return set(blogOption: "software_version", value: wordPressVersion) + set(blogOption: "software_version", value: wordPressVersion) } func with(username: String) -> Self { @@ -104,7 +104,8 @@ class BlogBuilder { func withAccount(username: String = "test_user") -> Self { // Add Account - let account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount + let account = + NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount account.keychain = MockKeychainService() account.keychainServiceName = "test-service" account.keychainMigration = MockAuthKeyMigration() @@ -152,7 +153,9 @@ class BlogBuilder { func with(domainCount: Int, of type: DomainType, domainName: String = "") -> Self { var domains: [ManagedDomain] = [] for _ in 0.. Self { + func withMappedDomain( + originalUrl: String = "http://domain1.com", + mappedDomainUrl: String = "http://domain2.com" + ) -> Self { set(blogOption: "unmapped_url", value: originalUrl) set(blogOption: "home_url", value: mappedDomainUrl) @@ -188,7 +194,7 @@ class BlogBuilder { } func with(isWPForTeamsSite: Bool) -> Self { - return set(blogOption: "is_wpforteams_site", value: isWPForTeamsSite) + set(blogOption: "is_wpforteams_site", value: isWPForTeamsSite) } func with(connections: Set) -> Self { @@ -205,7 +211,7 @@ class BlogBuilder { @discardableResult func build() -> Blog { - return blog + blog } @discardableResult @@ -232,7 +238,8 @@ extension Blog { return } - let account = NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount + let account = + NSEntityDescription.insertNewObject(forEntityName: WPAccount.entityName(), into: context) as! WPAccount account.keychain = MockKeychainService() account.keychainServiceName = "test-service" account.keychainMigration = MockAuthKeyMigration() diff --git a/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift b/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift index ebc8f7ebd3ac..9308893b4ea8 100644 --- a/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift +++ b/Modules/Tests/WordPressDataTests/Helpers/PostBuilder.swift @@ -132,7 +132,9 @@ class PostBuilder { return self } - guard let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media else { + guard + let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media + else { return self } media.localURL = image @@ -152,7 +154,7 @@ class PostBuilder { func with(media: [Media]) -> PostBuilder { for item in media { - item.blog = post.blog + item.blog = post.blog } post.media = Set(media) @@ -183,6 +185,6 @@ class PostBuilder { func build() -> Post { // TODO: Enable this assertion once we can ensure that the post's MOC isn't being deallocated after the `PostBuilder` is // assert(post.managedObjectContext != nil) - return post + post } } diff --git a/Tests/KeystoneTests/Helpers/PostBuilder.swift b/Tests/KeystoneTests/Helpers/PostBuilder.swift index d08cf1059307..e48ad17efa05 100644 --- a/Tests/KeystoneTests/Helpers/PostBuilder.swift +++ b/Tests/KeystoneTests/Helpers/PostBuilder.swift @@ -132,7 +132,9 @@ class PostBuilder { return self } - guard let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media else { + guard + let media = NSEntityDescription.insertNewObject(forEntityName: Media.entityName(), into: context) as? Media + else { return self } media.localURL = image @@ -152,7 +154,7 @@ class PostBuilder { func with(media: [Media]) -> PostBuilder { for item in media { - item.blog = post.blog + item.blog = post.blog } post.media = Set(media) @@ -183,6 +185,6 @@ class PostBuilder { func build() -> Post { // TODO: Enable this assertion once we can ensure that the post's MOC isn't being deallocated after the `PostBuilder` is // assert(post.managedObjectContext != nil) - return post + post } } diff --git a/Tests/KeystoneTests/Tests/Models/PostTests.swift b/Tests/KeystoneTests/Tests/Models/PostTests.swift index 983763bbcc7a..3c381c27c87f 100644 --- a/Tests/KeystoneTests/Tests/Models/PostTests.swift +++ b/Tests/KeystoneTests/Tests/Models/PostTests.swift @@ -7,15 +7,16 @@ import XCTest class PostTests: CoreDataTestCase { fileprivate func newTestBlog() -> Blog { - return NSEntityDescription.insertNewObject(forEntityName: Blog.entityName(), into: mainContext) as! Blog + NSEntityDescription.insertNewObject(forEntityName: Blog.entityName(), into: mainContext) as! Blog } fileprivate func newTestPost() -> Post { - return NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: mainContext) as! Post + NSEntityDescription.insertNewObject(forEntityName: Post.entityName(), into: mainContext) as! Post } fileprivate func newTestPostCategory() -> PostCategory { - return NSEntityDescription.insertNewObject(forEntityName: PostCategory.entityName(), into: mainContext) as! PostCategory + NSEntityDescription.insertNewObject(forEntityName: PostCategory.entityName(), into: mainContext) + as! PostCategory } fileprivate func newTestPostCategory(_ name: String) -> PostCategory { @@ -112,7 +113,11 @@ class PostTests: CoreDataTestCase { func testThatRemoveCategoriesWorks() { let post = newTestPost() - let testCategories = Set(arrayLiteral: newTestPostCategory("1"), newTestPostCategory("2"), newTestPostCategory("3")) + let testCategories = Set( + arrayLiteral: newTestPostCategory("1"), + newTestPostCategory("2"), + newTestPostCategory("3") + ) post.categories = testCategories XCTAssertNotEqual(post.categories?.count, 0) @@ -153,8 +158,10 @@ class PostTests: CoreDataTestCase { let post = newTestPost() let blog = newTestBlog() - blog.postFormats = [defaultPostFormat.key: defaultPostFormat.value, - secondaryPostFormat.key: secondaryPostFormat.value] + blog.postFormats = [ + defaultPostFormat.key: defaultPostFormat.value, + secondaryPostFormat.key: secondaryPostFormat.value + ] post.blog = blog post.postFormat = secondaryPostFormat.key @@ -169,8 +176,10 @@ class PostTests: CoreDataTestCase { let post = newTestPost() let blog = newTestBlog() - blog.postFormats = [defaultPostFormat.key: defaultPostFormat.value, - secondaryPostFormat.key: secondaryPostFormat.value] + blog.postFormats = [ + defaultPostFormat.key: defaultPostFormat.value, + secondaryPostFormat.key: secondaryPostFormat.value + ] post.blog = blog post.setPostFormatText(secondaryPostFormat.value) @@ -185,7 +194,7 @@ class PostTests: CoreDataTestCase { "z": "Z", "standard": "Standard", "a": "A", - "d": "D", + "d": "D" ] let expectedPostFormats = [ From 7b3ebf91581da76b01f420d8058418bcd488bd9b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 16:28:24 +1200 Subject: [PATCH 8/9] Remove the unused socialIcon(for:) style helper --- .../ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift index 64aac9fd82d9..b5374bf14986 100644 --- a/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift +++ b/WordPress/Classes/ViewRelated/Blog/Sharing/WPStyleGuide+Sharing.swift @@ -37,8 +37,4 @@ extension WPStyleGuide { } return image!.withRenderingMode(.alwaysTemplate) } - - public class func socialIcon(for service: NSString) -> UIImage { - UIImage(named: "icon-\(service)") ?? iconForService(service) - } } From 7f9fc60c85e8a2806a395aec3d2b7a07fb24be6e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 6 Jul 2026 16:52:09 +1200 Subject: [PATCH 9/9] Clean up leftover publicize references in comments and strings Fixes two stale copy-paste comments in the sharing buttons service and removes an unused Jetpack Social string from the pre-publishing sheet. --- WordPress/Classes/Services/SharingService.swift | 4 ++-- .../Post/Publishing/PublishPostViewController.swift | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/Services/SharingService.swift b/WordPress/Classes/Services/SharingService.swift index 5a59346c5700..b53a0ce243b6 100644 --- a/WordPress/Classes/Services/SharingService.swift +++ b/WordPress/Classes/Services/SharingService.swift @@ -98,7 +98,7 @@ import WordPressKit self.createOrReplaceFromRemoteSharingButton(remoteButton, blog: blog, in: context) } - // Delete any cached PublicizeServices that were not synced. + // Delete any cached SharingButtons that were not synced. for button in currentSharingbuttons { if !buttonsToKeep.contains(button) { context.delete(button) @@ -116,7 +116,7 @@ import WordPressKit /// data represented by the passed `RemoteSharingButton`. /// /// - Parameters: - /// - remoteButton: The remote connection representing the publicize connection. + /// - remoteButton: The remote sharing button to create or update from. /// - blog: The `Blog` that owns or will own the button. /// /// - Returns: A `SharingButton`. diff --git a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift index 92e4a35b415a..d70e0aef88b1 100644 --- a/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Publishing/PublishPostViewController.swift @@ -263,11 +263,6 @@ enum PrepublishingSheetStrings { value: "Tags", comment: "Label for a cell in the pre-publishing sheet" ) - static let jetpackSocial = NSLocalizedString( - "prepublishing.jetpackSocial", - value: "Jetpack Social", - comment: "Label for a cell in the pre-publishing sheet" - ) static let immediately = NSLocalizedString( "prepublishing.publishDateImmediately", value: "Immediately",