diff --git a/FBSDKShareKit/FBSDKShareKit/Content/_ShareUtility.swift b/FBSDKShareKit/FBSDKShareKit/Content/_ShareUtility.swift index c887765456..38367475c3 100644 --- a/FBSDKShareKit/FBSDKShareKit/Content/_ShareUtility.swift +++ b/FBSDKShareKit/FBSDKShareKit/Content/_ShareUtility.swift @@ -163,7 +163,7 @@ extension _ShareUtility: ShareUtilityProtocol { guard let linkContent = content as? ShareLinkContent else { return nil } let parameters: [String: Any?] = [ - "link": linkContent.contentURL, + "href": linkContent.contentURL?.absoluteString, "quote": linkContent.quote, "hashtag": hashtagString(from: linkContent.hashtag), "place": content.placeID, diff --git a/FBSDKShareKit/FBSDKShareKit/PrivacyInfo.xcprivacy b/FBSDKShareKit/FBSDKShareKit/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..e69128a849 --- /dev/null +++ b/FBSDKShareKit/FBSDKShareKit/PrivacyInfo.xcprivacy @@ -0,0 +1,52 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDataTypes + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + ep1.facebook.com + + + diff --git a/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialog.swift b/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialog.swift index 81af991c6d..2c080cbd9b 100644 --- a/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialog.swift +++ b/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialog.swift @@ -20,7 +20,7 @@ public class ShareDialog: NSObject, SharingDialog { // swiftlint:disable:this pr private struct UnknownValidationError: Error {} private struct BridgeRequestCreationError: Error {} - private static let feedMethodName = "feed" + private static let feedMethodName = "share" private static var hasValidatedURLSchemeRegistration = false private static var temporaryDirectory = URL( @@ -172,7 +172,12 @@ extension ShareDialog { public var canShow: Bool { guard shareContent != nil else { - return canShowWithoutContent + do { + try validateAdvertiserTrackingPermission(mode: mode) + return canShowWithoutContent + } catch { + return false + } } do { @@ -430,7 +435,7 @@ extension ShareDialog { throw MissingContentError() } - try validateShareContentForBrowser() + try validateShareContentForShareWebDialog(mode: .browser) if let photoContent = content as? SharePhotoContent, photoContentHasAtLeastOneImage(photoContent) { @@ -490,7 +495,7 @@ extension ShareDialog { } private func showFeedBrowser() throws { - try validateShareContentForFeed() + try validateShareContentForFeedWebDialog(mode: .feedBrowser) let dependencies = try Self.getDependencies() guard let content = shareContent else { @@ -525,7 +530,7 @@ extension ShareDialog { } private func showFeedWeb() throws { - try validateShareContentForFeed() + try validateShareContentForFeedWebDialog(mode: .feedWeb) let dependencies = try Self.getDependencies() guard let content = shareContent else { @@ -680,7 +685,7 @@ extension ShareDialog { } private func showWeb() throws { - try validateShareContentForBrowser(options: .photoImageURL) + try validateShareContentForShareWebDialog(mode: .web, options: .photoImageURL) let dependencies = try Self.getDependencies() guard let content = shareContent else { @@ -748,12 +753,13 @@ extension ShareDialog { case .shareSheet: try validateShareContentForShareSheet() case .browser: - try validateShareContentForBrowser() + try validateShareContentForShareWebDialog(mode: mode) case .web: - try validateShareContentForBrowser(options: .photoImageURL) - case .feedBrowser, - .feedWeb: - try validateShareContentForFeed() + try validateShareContentForShareWebDialog(mode: mode, options: .photoImageURL) + case .feedBrowser: + try validateShareContentForFeedWebDialog(mode: mode) + case .feedWeb: + try validateShareContentForFeedWebDialog(mode: mode) } } @@ -773,23 +779,29 @@ extension ShareDialog { } do { - try validateShareContentForFeed() + try validateShareContentForFeedWebDialog(mode: .automatic) return } catch {} - try validateShareContentForBrowser() + try validateShareContentForShareWebDialog(mode: .automatic) } - private func validateShareContentForBrowser(options bridgeOptions: ShareBridgeOptions = []) throws { + private func validateShareContentForShareWebDialog( + mode: Mode, + options bridgeOptions: ShareBridgeOptions = [] + ) throws { let dependencies = try Self.getDependencies() + try validateAdvertiserTrackingPermission(mode: mode) + guard let content = shareContent else { throw MissingContentError() } + let errorFactory = dependencies.errorFactory if let linkContent = content as? ShareLinkContent, linkContent.contentURL == nil { - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: linkContent, @@ -799,7 +811,7 @@ extension ShareDialog { } guard !(shareContent is ShareCameraEffectContent) else { - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: shareContent, @@ -812,7 +824,7 @@ extension ShareDialog { if flags.containsPhotos { guard AccessToken.current != nil else { - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: content, @@ -824,7 +836,7 @@ extension ShareDialog { if let photo = content as? SharePhotoContent { try photo.validate(options: bridgeOptions) } else { - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: content, @@ -835,18 +847,18 @@ extension ShareDialog { } if flags.containsVideos { - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: content, - message: "video sharing through the browser is not supported.", + message: "Video sharing through the browser is not supported.", underlyingError: nil ) } if flags.containsMedia, bridgeOptions == .photoImageURL { // a web-based URL is required - throw dependencies.errorFactory.invalidArgumentError( + throw errorFactory.invalidArgumentError( domain: ShareErrorDomain, name: "shareContent", value: content, @@ -856,8 +868,11 @@ extension ShareDialog { } } - private func validateShareContentForFeed() throws { - let errorFactory = try Self.getDependencies().errorFactory + private func validateShareContentForFeedWebDialog(mode: Mode) throws { + let dependencies = try Self.getDependencies() + let errorFactory = dependencies.errorFactory + + try validateAdvertiserTrackingPermission(mode: mode) if let linkContent = shareContent as? ShareLinkContent { if linkContent.contentURL == nil { @@ -874,7 +889,7 @@ extension ShareDialog { domain: ShareErrorDomain, name: "shareContent", value: shareContent, - message: "Feed share dialogs support ShareLinkContent.", + message: "Feed share dialogs support ShareLinkContent only.", underlyingError: nil ) } @@ -965,6 +980,34 @@ extension ShareDialog { } } + private func validateAdvertiserTrackingPermission(mode: Mode) throws { + let dependencies = try Self.getDependencies() + let errorFactory = dependencies.errorFactory + + let isTrackingEnabled = dependencies.settings.isAdvertiserTrackingEnabled == true + let isWebViewsMode = mode == .feedWeb || mode == .web + guard !isWebViewsMode || isTrackingEnabled else { + throw errorFactory.invalidArgumentError( + domain: ShareErrorDomain, + name: "unavailableDestination", + value: shareContent, + message: "Tracking permission is required to share to web destination.", + underlyingError: nil + ) + } + + let isBrowsersMode = mode == .browser || mode == .feedBrowser + guard !isBrowsersMode || !(shareContent is SharePhotoContent) || isTrackingEnabled else { + throw errorFactory.invalidArgumentError( + domain: ShareErrorDomain, + name: "unavailableDestination", + value: shareContent, + message: "Valid access token is required to share photos with browser destination.", + underlyingError: nil + ) + } + } + private func invokeDelegateDidCancel() { AppEvents.shared.logInternalEvent( .shareDialogResult, diff --git a/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialogMode.swift b/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialogMode.swift index 63a2ee774f..9fb34f3b4d 100644 --- a/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialogMode.swift +++ b/FBSDKShareKit/FBSDKShareKit/UserInterface/ShareDialogMode.swift @@ -30,12 +30,27 @@ extension ShareDialog { case browser /// Displays the dialog in a WKWebView within the app. + @available( + *, + deprecated, + message: "The web sharing mode is deprecated. Consider using automatic sharing mode instead." + ) case web /// Displays the feed dialog in Safari. + @available( + *, + deprecated, + message: "The feed browser sharing mode is deprecated. Consider using automatic or browser sharing modes instead." + ) case feedBrowser /// Displays the feed dialog in a WKWebView within the app. + @available( + *, + deprecated, + message: "The feed web sharing mode is deprecated. Consider using automatic sharing mode instead." + ) case feedWeb /// The string description diff --git a/FBSDKShareKit/FBSDKShareKitTests/UserInterface/ShareDialogTests.swift b/FBSDKShareKit/FBSDKShareKitTests/UserInterface/ShareDialogTests.swift index 8bb8f1a726..7eb506a474 100644 --- a/FBSDKShareKit/FBSDKShareKitTests/UserInterface/ShareDialogTests.swift +++ b/FBSDKShareKit/FBSDKShareKitTests/UserInterface/ShareDialogTests.swift @@ -211,6 +211,8 @@ final class ShareDialogTests: XCTestCase { let controller = UIViewController() let content = ShareModelTestUtility.linkContent let delegate = TestSharingDelegate() + let components = WebShareBridgeComponents(methodName: "test", parameters: ["key": "value"]) + TestShareUtility.stubbedWebShareBridgeComponents = components dialog = ShareDialog.show(viewController: controller, content: content, delegate: delegate) XCTAssertIdentical(dialog.fromViewController, controller, .Construction.createViaClassShowMethod) @@ -305,6 +307,7 @@ final class ShareDialogTests: XCTestCase { AccessToken.current = SampleAccessTokens.validToken dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs + settings.isAdvertiserTrackingEnabled = true XCTAssertTrue( dialog.canShow, "A dialog with photo content with file urls should be showable in a browser when there is a current access token" @@ -323,6 +326,7 @@ final class ShareDialogTests: XCTestCase { dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) + settings.isAdvertiserTrackingEnabled = true dialog.shareContent = ShareModelTestUtility.photoContentWithImages AccessToken.current = SampleAccessTokens.validToken XCTAssertNoThrow(try dialog.validate()) @@ -397,6 +401,7 @@ final class ShareDialogTests: XCTestCase { } func testSharingViaBrowserWithValidPhotoContent() { + settings.isAdvertiserTrackingEnabled = true let request = TestBridgeAPIRequest() bridgeAPIRequestFactory.stubbedBridgeAPIRequest = request let components = WebShareBridgeComponents(methodName: "test", parameters: ["key": "value"]) @@ -442,7 +447,8 @@ final class ShareDialogTests: XCTestCase { // MARK: - Web mode - func testCanShowWeb() { + func testCanShowWebWithTrackingPermission() { + settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .web) XCTAssertTrue( dialog.canShow, @@ -473,7 +479,22 @@ final class ShareDialogTests: XCTestCase { ) } - func testValidateWeb() throws { + func testCanShowWebWithoutTrackingPermissions() { + dialog = createEmptyDialog(mode: .web) + XCTAssertFalse( + dialog.canShow, + "A dialog without share content should be showable on web" + ) + + dialog.shareContent = ShareModelTestUtility.linkContent + XCTAssertFalse( + dialog.canShow, + "A dialog with link content should not be showable on web" + ) + } + + func testValidateWebWithTrackingPermission() throws { + settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .web) dialog.shareContent = ShareModelTestUtility.linkContent @@ -660,7 +681,8 @@ final class ShareDialogTests: XCTestCase { // MARK: - Feed web mode - func testCanShowFeedWeb() { + func testCanShowFeedWebWithTrackingPermission() { + settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .feedWeb) XCTAssertTrue( @@ -687,7 +709,18 @@ final class ShareDialogTests: XCTestCase { ) } - func testValidateFeedWeb() throws { + func testCanShowFeedWebWithoutTrackingPermission() { + settings.isAdvertiserTrackingEnabled = false + dialog = createEmptyDialog(mode: .feedWeb) + + XCTAssertFalse( + dialog.canShow, + "A dialog without tracking permissions should not be showable in a web feed" + ) + } + + func testValidateFeedWebWithTrackingPermission() throws { + settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .feedWeb) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) @@ -699,6 +732,16 @@ final class ShareDialogTests: XCTestCase { XCTAssertThrowsError(try dialog.validate()) } + func testValidateFeedWebWithoutTrackingPermission() throws { + settings.isAdvertiserTrackingEnabled = false + dialog = createEmptyDialog(mode: .feedWeb) + dialog.shareContent = ShareModelTestUtility.linkContent + XCTAssertThrowsError( + try dialog.validate(), + "A web view dialog without tracking permission should not pass validation" + ) + } + // MARK: - Automatic mode func testCameraShareModesWhenNativeUnavailable() { diff --git a/Package.swift b/Package.swift index 86a210aa7d..80606107f7 100644 --- a/Package.swift +++ b/Package.swift @@ -175,7 +175,13 @@ extension Target { ] ) - static let share = target(name: .share, dependencies: [.core, .Prefixed.share]) + static let share = target( + name: .share, + dependencies: [.core, .Prefixed.share], + resources: [ + .copy("Resources/PrivacyInfo.xcprivacy"), + ] + ) static let gaming = target(name: .gaming, dependencies: [.Prefixed.gaming]) diff --git a/Sources/FacebookShare/Resources/PrivacyInfo.xcprivacy b/Sources/FacebookShare/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..e69128a849 --- /dev/null +++ b/Sources/FacebookShare/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,52 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDataTypes + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + ep1.facebook.com + + +