diff --git a/Sources/MapboxMaps/Style/AttributionDialogManager.swift b/Sources/MapboxMaps/Style/AttributionDialogManager.swift index 3342e6ba6d2f..9506eac0f2d0 100644 --- a/Sources/MapboxMaps/Style/AttributionDialogManager.swift +++ b/Sources/MapboxMaps/Style/AttributionDialogManager.swift @@ -1,108 +1,109 @@ import UIKit -internal protocol AttributionDataSource: AnyObject { +import Foundation +@_implementationOnly import MapboxCommon_Private + +protocol AttributionDataSource: AnyObject { func loadAttributions(completion: @escaping ([Attribution]) -> Void) } -internal protocol AttributionDialogManagerDelegate: AnyObject { +protocol AttributionDialogManagerDelegate: AnyObject { func viewControllerForPresenting(_ attributionDialogManager: AttributionDialogManager) -> UIViewController? func attributionDialogManager(_ attributionDialogManager: AttributionDialogManager, didTriggerActionFor attribution: Attribution) } -internal class AttributionDialogManager { - +final class AttributionDialogManager { private weak var dataSource: AttributionDataSource? private weak var delegate: AttributionDialogManagerDelegate? private var inProcessOfParsingAttributions: Bool = false - internal init(dataSource: AttributionDataSource, delegate: AttributionDialogManagerDelegate?) { + private let isGeofenceActive: () -> Bool + private let setGeofenceConsent: (Bool) -> Void + private let getGeofenceConsent: () -> Bool + + init( + dataSource: AttributionDataSource, + delegate: AttributionDialogManagerDelegate?, + isGeofenceActive: @escaping () -> Bool = { __GeofencingUtils.isActive() }, + setGeofenceConsent: @escaping (Bool) -> Void = { isConsentGiven in + __GeofencingUtils.setUserConsent(isConsentGiven: isConsentGiven, callback: { expected in + if let error = expected.error { Log.error(forMessage: "Error: \(error) occurred while changing user consent for Geofencing.") } + }) + }, + getGeofenceConsent: @escaping () -> Bool = { __GeofencingUtils.getUserConsent() } + ) { self.dataSource = dataSource self.delegate = delegate + self.isGeofenceActive = isGeofenceActive + self.setGeofenceConsent = setGeofenceConsent + self.getGeofenceConsent = getGeofenceConsent } - internal var isMetricsEnabled: Bool { - get { - UserDefaults.standard.MGLMapboxMetricsEnabled - } - set { - UserDefaults.standard.MGLMapboxMetricsEnabled = newValue - } + var isMetricsEnabled: Bool { + get { UserDefaults.standard.MGLMapboxMetricsEnabled } + set { UserDefaults.standard.MGLMapboxMetricsEnabled = newValue } } - //swiftlint:disable:next function_body_length - internal func showTelemetryAlertController(from viewController: UIViewController) { - let alert: UIAlertController - let bundle = Bundle.mapboxMaps - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Make Mapbox Maps Better", - comment: "Telemetry prompt title") - - let message: String + func showGeofencingAlertController(from viewController: UIViewController) { + let telemetryTitle = GeofencingStrings.geofencingTitle + let message = GeofencingStrings.geofencingMessage let participateTitle: String let declineTitle: String - if isMetricsEnabled { - message = NSLocalizedString("TELEMETRY_ENABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: """ - You are helping to make OpenStreetMap and - Mapbox maps better by contributing anonymous usage data. - """, - comment: "Telemetry prompt message") - participateTitle = NSLocalizedString("TELEMETRY_ENABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Keep Participating", - comment: "Telemetry prompt button") - declineTitle = NSLocalizedString("TELEMETRY_ENABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Stop Participating", - comment: "Telemetry prompt button") + if getGeofenceConsent() { + participateTitle = GeofencingStrings.geofencingEnabledOnMessage + declineTitle = GeofencingStrings.geofencingEnabledOffMessage } else { - message = NSLocalizedString("TELEMETRY_DISABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: """ - You can help make OpenStreetMap and Mapbox maps better - by contributing anonymous usage data. - """, - comment: "Telemetry prompt message") - participateTitle = NSLocalizedString("TELEMETRY_DISABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Participate", - comment: "Telemetry prompt button") - declineTitle = NSLocalizedString("TELEMETRY_DISABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Don’t Participate", - comment: "Telemetry prompt button") + participateTitle = GeofencingStrings.geofencingDisabledOnMessage + declineTitle = GeofencingStrings.geofencingDisabledOffMessage } - if UIDevice.current.userInterfaceIdiom == .pad { - alert = UIAlertController(title: telemetryTitle, message: message, preferredStyle: .alert) + showAlertController(from: viewController, title: telemetryTitle, message: message, actions: [ + UIAlertAction(title: declineTitle, style: .default, handler: { _ in self.setGeofenceConsent(false) }), + UIAlertAction(title: participateTitle, style: .cancel, handler: { _ in self.setGeofenceConsent(true) }) + ]) + } + + func showTelemetryAlertController(from viewController: UIViewController) { + let telemetryTitle = TelemetryStrings.telemetryTitle + let message: String + let participateTitle: String + let declineTitle: String + + if isMetricsEnabled { + message = TelemetryStrings.telemetryEnabledMessage + participateTitle = TelemetryStrings.telemetryEnabledOnMessage + declineTitle = TelemetryStrings.telemetryEnabledOffMessage } else { - alert = UIAlertController(title: telemetryTitle, message: message, preferredStyle: .actionSheet) + message = TelemetryStrings.telemetryDisabledMessage + participateTitle = TelemetryStrings.telemetryDisabledOnMessage + declineTitle = TelemetryStrings.telemetryDisabledOffMessage } - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, value: "Tell Me More", - comment: "Telemetry prompt button") - let moreAction = UIAlertAction(title: moreTitle, style: .default) { _ in + let openTelemetryURL: (UIAlertAction) -> Void = { _ in guard let url = URL(string: Ornaments.telemetryURL) else { return } self.delegate?.attributionDialogManager(self, didTriggerActionFor: Attribution(title: "", url: url)) } - alert.addAction(moreAction) - alert.addAction(UIAlertAction(title: declineTitle, style: .default) { _ in - self.isMetricsEnabled = false - }) + showAlertController(from: viewController, title: telemetryTitle, message: message, actions: [ + UIAlertAction(title: TelemetryStrings.telemetryMore, style: .default, handler: openTelemetryURL), + UIAlertAction(title: declineTitle, style: .default, handler: { _ in self.isMetricsEnabled = false }), + UIAlertAction(title: participateTitle, style: .cancel, handler: { _ in self.isMetricsEnabled = true }) + ]) + } - alert.addAction(UIAlertAction(title: participateTitle, style: .cancel) { _ in - self.isMetricsEnabled = true - }) + func showAlertController( + from viewController: UIViewController, + title: String, + message: String, + actions: [UIAlertAction] + ) { + let alert = if UIDevice.current.userInterfaceIdiom == .pad { + UIAlertController(title: title, message: message, preferredStyle: .alert) + } else { + UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + } + actions.forEach(alert.addAction) viewController.present(alert, animated: true) } } @@ -150,17 +151,19 @@ extension AttributionDialogManager: InfoButtonOrnamentDelegate { } } - let telemetryTitle = NSLocalizedString("TELEMETRY_NAME", - tableName: Ornaments.localizableTableName, - bundle: bundle, - value: "Mapbox Telemetry", - comment: "Action in attribution sheet") - let telemetryAction = UIAlertAction(title: telemetryTitle, style: .default) { _ in + let telemetryAction = UIAlertAction(title: TelemetryStrings.telemetryName, style: .default) { _ in self.showTelemetryAlertController(from: viewController) } alert.addAction(telemetryAction) + if isGeofenceActive() || !getGeofenceConsent() { + let geofencingAction = UIAlertAction(title: GeofencingStrings.geofencingName, style: .default) { _ in + self.showGeofencingAlertController(from: viewController) + } + alert.addAction(geofencingAction) + } + let privacyPolicyAttribution = Attribution.makePrivacyPolicyAttribution() let privacyPolicyAction = UIAlertAction(title: privacyPolicyAttribution.title, style: .default) { _ in self.delegate?.attributionDialogManager(self, didTriggerActionFor: privacyPolicyAttribution) @@ -191,3 +194,145 @@ private extension Attribution { ) } } + +enum TelemetryStrings { + static let telemetryName = NSLocalizedString( + "TELEMETRY_NAME", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Mapbox Telemetry", + comment: "Action in attribution sheet" + ) + + static let telemetryTitle = NSLocalizedString( + "TELEMETRY_TITLE", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Make Mapbox Maps Better", + comment: "Telemetry prompt title" + ) + + static let telemetryEnabledMessage = NSLocalizedString( + "TELEMETRY_ENABLED_MSG", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: """ + You are helping to make OpenStreetMap and + Mapbox maps better by contributing anonymous usage data. + """, + comment: "Telemetry prompt message" + ) + + static let telemetryDisabledMessage = NSLocalizedString( + "TELEMETRY_DISABLED_MSG", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: """ + You can help make OpenStreetMap and Mapbox maps better + by contributing anonymous usage data. + """, + comment: "Telemetry prompt message" + ) + + static let telemetryEnabledOnMessage = NSLocalizedString( + "TELEMETRY_ENABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Keep Participating", + comment: "Telemetry prompt button" + ) + + static let telemetryEnabledOffMessage = NSLocalizedString( + "TELEMETRY_ENABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Stop Participating", + comment: "Telemetry prompt button" + ) + + static let telemetryDisabledOnMessage = NSLocalizedString( + "TELEMETRY_DISABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Participate", + comment: "Telemetry prompt button" + ) + + static let telemetryDisabledOffMessage = NSLocalizedString( + "TELEMETRY_DISABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Don’t Participate", + comment: "Telemetry prompt button" + ) + + static let telemetryMore = NSLocalizedString( + "TELEMETRY_MORE", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Tell Me More", + comment: "Telemetry prompt button" + ) +} + +enum GeofencingStrings { + static let geofencingName = NSLocalizedString( + "GEOFENCING_NAME", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Mapbox Geofencing", + comment: "Action in attribution sheet" + ) + + static let geofencingTitle = NSLocalizedString( + "GEOFENCING_TITLE", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Allow This App to Use Geofencing", + comment: "Geofencing prompt title" + ) + + static let geofencingMessage = NSLocalizedString( + "GEOFENCING_MSG", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: """ + This app uses Mapbox Geofencing to detect your device’s presence in areas the app developer has defined. + Only the app developer can see where those areas are. + You have the option to disable Mapbox Geofencing, which may affect app functionality. + """, + comment: "Geofencing prompt message" + ) + + static let geofencingEnabledOnMessage = NSLocalizedString( + "GEOFENCING_ENABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Keep Geofencing enabled", + comment: "Geofencing prompt button" + ) + + static let geofencingEnabledOffMessage = NSLocalizedString( + "GEOFENCING_ENABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Disable Geofencing", + comment: "Geofencing prompt button" + ) + + static let geofencingDisabledOnMessage = NSLocalizedString( + "GEOFENCING_DISABLED_ON", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Enable Geofencing", + comment: "Geofencing prompt button" + ) + + static let geofencingDisabledOffMessage = NSLocalizedString( + "GEOFENCING_DISABLED_OFF", + tableName: Ornaments.localizableTableName, + bundle: .mapboxMaps, + value: "Keep Geofencing Disabled", + comment: "Geofencing prompt button" + ) +} diff --git a/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift b/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift index 911fbc57f5d8..6d9c867fc2fb 100644 --- a/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift +++ b/Tests/MapboxMapsTests/Ornaments/InfoButton/InfoButtonOrnamentTests.swift @@ -7,10 +7,19 @@ class InfoButtonOrnamentTests: XCTestCase { var attributionDialogManager: AttributionDialogManager! var tapCompletion: (() -> Void)? + private var isGeofenceActive: Bool = false + private var isGeofenceConsentGiven: Bool = true + override func setUp() { super.setUp() parentViewController = MockParentViewController() - attributionDialogManager = AttributionDialogManager(dataSource: self, delegate: self) + attributionDialogManager = AttributionDialogManager( + dataSource: self, + delegate: self, + isGeofenceActive: { self.isGeofenceActive }, + setGeofenceConsent: { self.isGeofenceConsentGiven = $0 }, + getGeofenceConsent: { self.isGeofenceConsentGiven } + ) } func testInfoButtonTapped() throws { diff --git a/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift b/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift index 103a29beaf0f..b03206d97dee 100644 --- a/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift +++ b/Tests/MapboxMapsTests/Style/AttributionDialogTests.swift @@ -4,16 +4,23 @@ import Foundation import UIKit class AttributionDialogTests: XCTestCase { - var attributionDialogManager: AttributionDialogManager! var mockDataSource: MockAttributionDataSource! var mockDelegate: MockAttributionDialogManagerDelegate! + private var isGeofenceActive: Bool = false + private var isGeofenceConsentGiven: Bool = true override func setUp() { super.setUp() mockDataSource = MockAttributionDataSource() mockDelegate = MockAttributionDialogManagerDelegate() - attributionDialogManager = AttributionDialogManager(dataSource: mockDataSource, delegate: mockDelegate) + attributionDialogManager = AttributionDialogManager( + dataSource: mockDataSource, + delegate: mockDelegate, + isGeofenceActive: { self.isGeofenceActive }, + setGeofenceConsent: { self.isGeofenceConsentGiven = $0 }, + getGeofenceConsent: { self.isGeofenceConsentGiven } + ) } override func tearDown() { @@ -24,27 +31,77 @@ class AttributionDialogTests: XCTestCase { mockDelegate = nil } - func testShowTelemetryDialogMetricsEnabled() throws { + func testShowGeofencingDialogGeofencingEnabled() throws { + let viewController = UIViewController() + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() + isGeofenceConsentGiven = true + + attributionDialogManager.showGeofencingAlertController(from: viewController) + + let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) + let geofenceTitle = GeofencingStrings.geofencingTitle + XCTAssertEqual(alert.title, geofenceTitle) + + let message = GeofencingStrings.geofencingMessage + XCTAssertEqual(alert.message, message) + + guard alert.actions.count == 2 else { + XCTFail("Telemetry alert should have 2 actions") + return + } + + let declineTitle = GeofencingStrings.geofencingEnabledOffMessage + XCTAssertEqual(alert.actions[0].title, declineTitle) + + let participateTitle = GeofencingStrings.geofencingEnabledOnMessage + XCTAssertEqual(alert.actions[1].title, participateTitle) + } + + func testShowGeofencingDialogGeofencingDisabled() throws { let viewController = UIViewController() let bundle = Bundle.mapboxMaps let window = UIWindow() window.rootViewController = viewController window.makeKeyAndVisible() + isGeofenceConsentGiven = false + + attributionDialogManager.showGeofencingAlertController(from: viewController) + + let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) + let geofenceTitle = GeofencingStrings.geofencingTitle + XCTAssertEqual(alert.title, geofenceTitle) + + let message = GeofencingStrings.geofencingMessage + XCTAssertEqual(alert.message, message) + + guard alert.actions.count == 2 else { + XCTFail("Telemetry alert should have 2 actions") + return + } + + let declineTitle = GeofencingStrings.geofencingDisabledOffMessage + XCTAssertEqual(alert.actions[0].title, declineTitle) + + let participateTitle = GeofencingStrings.geofencingDisabledOnMessage + XCTAssertEqual(alert.actions[1].title, participateTitle) + } + + func testShowTelemetryDialogMetricsEnabled() throws { + let viewController = UIViewController() + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() attributionDialogManager.isMetricsEnabled = true attributionDialogManager.showTelemetryAlertController(from: viewController) let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let telemetryTitle = TelemetryStrings.telemetryTitle XCTAssertEqual(alert.title, telemetryTitle) - let message = NSLocalizedString("TELEMETRY_ENABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let message = TelemetryStrings.telemetryEnabledMessage XCTAssertEqual(alert.message, message) guard alert.actions.count == 3 else { @@ -52,22 +109,13 @@ class AttributionDialogTests: XCTestCase { return } - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let moreTitle = TelemetryStrings.telemetryMore XCTAssertEqual(alert.actions[0].title, moreTitle) - let declineTitle = NSLocalizedString("TELEMETRY_ENABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let declineTitle = TelemetryStrings.telemetryEnabledOffMessage XCTAssertEqual(alert.actions[1].title, declineTitle) - let participateTitle = NSLocalizedString("TELEMETRY_ENABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let participateTitle = TelemetryStrings.telemetryEnabledOnMessage XCTAssertEqual(alert.actions[2].title, participateTitle) } @@ -82,16 +130,10 @@ class AttributionDialogTests: XCTestCase { attributionDialogManager.showTelemetryAlertController(from: viewController) let alert = try XCTUnwrap(viewController.presentedViewController as? UIAlertController) - let telemetryTitle = NSLocalizedString("TELEMETRY_TITLE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let telemetryTitle = TelemetryStrings.telemetryTitle XCTAssertEqual(alert.title, telemetryTitle) - let message = NSLocalizedString("TELEMETRY_DISABLED_MSG", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let message = TelemetryStrings.telemetryDisabledMessage XCTAssertEqual(alert.message, message) guard alert.actions.count == 3 else { @@ -99,22 +141,13 @@ class AttributionDialogTests: XCTestCase { return } - let moreTitle = NSLocalizedString("TELEMETRY_MORE", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let moreTitle = TelemetryStrings.telemetryMore XCTAssertEqual(alert.actions[0].title, moreTitle) - let declineTitle = NSLocalizedString("TELEMETRY_DISABLED_OFF", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let declineTitle = TelemetryStrings.telemetryDisabledOffMessage XCTAssertEqual(alert.actions[1].title, declineTitle) - let participateTitle = NSLocalizedString("TELEMETRY_DISABLED_ON", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let participateTitle = TelemetryStrings.telemetryDisabledOnMessage XCTAssertEqual(alert.actions[2].title, participateTitle) } @@ -141,10 +174,7 @@ class AttributionDialogTests: XCTestCase { return } - let telemetryTitle = NSLocalizedString("TELEMETRY_NAME", - tableName: Ornaments.localizableTableName, - bundle: bundle, - comment: "") + let telemetryTitle = TelemetryStrings.telemetryName XCTAssertEqual(alert.actions[0].title, telemetryTitle) XCTAssertEqual(alert.actions[1].title, Attribution.makePrivacyPolicyAttribution().title)