Skip to content

Commit f896928

Browse files
authored
Merge pull request #349 from woocommerce/issue/218-privacy-settings
Settings: Privacy Settings
2 parents b16d901 + 10b3848 commit f896928

20 files changed

+752
-100
lines changed

WooCommerce/Classes/Analytics/AnalyticsProvider.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,19 @@ public protocol AnalyticsProvider {
1414
func track(_ eventName: String)
1515

1616

17-
/// Track a spcific event with associated properties
17+
/// Track a specific event with associated properties
1818
///
1919
/// - Parameters:
2020
/// - eventName: the event name
2121
/// - properties: a collection of properties
2222
///
2323
func track(_ eventName: String, withProperties properties: [AnyHashable: Any]?)
24+
25+
/// Clear queued events
26+
///
27+
func clearEvents()
28+
29+
/// Switch between an authed user and anon user
30+
///
31+
func clearUsers()
2432
}

WooCommerce/Classes/Analytics/TracksProvider.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ public extension TracksProvider {
4141
DDLogInfo("🔵 Tracked \(eventName)")
4242
}
4343
}
44+
45+
func clearEvents() {
46+
tracksService.clearQueuedEvents()
47+
}
48+
49+
/// When a user opts-out, wipe data
50+
///
51+
func clearUsers() {
52+
guard WooAnalytics.shared.userHasOptedIn else {
53+
// To be safe, nil out the anonymousUserID guid so a fresh one is regenerated
54+
UserDefaults.standard[.defaultAnonymousID] = nil
55+
UserDefaults.standard[.analyticsUsername] = nil
56+
tracksService.switchToAnonymousUser(withAnonymousID: StoresManager.shared.sessionManager.anonymousUserID)
57+
return
58+
}
59+
60+
switchTracksUsersIfNeeded()
61+
}
4462
}
4563

4664

WooCommerce/Classes/Analytics/WooAnalytics.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ public class WooAnalytics {
1818
///
1919
private var applicationOpenedTime: Date?
2020

21+
/// Check user opt-in for analytics
22+
///
23+
var userHasOptedIn: Bool {
24+
get {
25+
let optedIn: Bool? = UserDefaults.standard.object(forKey: .userOptedInAnalytics)
26+
return optedIn ?? true // analytics tracking on by default
27+
}
28+
set {
29+
UserDefaults.standard.set(newValue, forKey: .userOptedInAnalytics)
30+
}
31+
}
32+
2133

2234
// MARK: - Initialization
2335

@@ -48,6 +60,10 @@ public extension WooAnalytics {
4860
/// It's good to call this function after a user logs in or out of the app.
4961
///
5062
func refreshUserData() {
63+
guard userHasOptedIn == true else {
64+
return
65+
}
66+
5167
analyticsProvider.refreshUserData()
5268
}
5369

@@ -56,6 +72,10 @@ public extension WooAnalytics {
5672
/// - Parameter stat: the event name
5773
///
5874
func track(_ stat: WooAnalyticsStat) {
75+
guard userHasOptedIn == true else {
76+
return
77+
}
78+
5979
track(stat, withProperties: nil)
6080
}
6181

@@ -66,6 +86,10 @@ public extension WooAnalytics {
6686
/// - properties: a collection of properties related to the event
6787
///
6888
func track(_ stat: WooAnalyticsStat, withProperties properties: [AnyHashable: Any]?) {
89+
guard userHasOptedIn == true else {
90+
return
91+
}
92+
6993
if let updatedProperties = updatePropertiesIfNeeded(for: stat, properties: properties) {
7094
analyticsProvider.track(stat.rawValue, withProperties: updatedProperties)
7195
} else {
@@ -80,6 +104,10 @@ public extension WooAnalytics {
80104
/// - error: the error to track
81105
///
82106
func track(_ stat: WooAnalyticsStat, withError error: Error) {
107+
guard userHasOptedIn == true else {
108+
return
109+
}
110+
83111
let err = error as NSError
84112
let errorDictionary = [Constants.errorKeyCode: "\(err.code)",
85113
Constants.errorKeyDomain: err.domain,
@@ -90,15 +118,45 @@ public extension WooAnalytics {
90118
}
91119

92120

121+
// MARK: - Opt Out
122+
//
123+
extension WooAnalytics {
124+
125+
func setUserHasOptedIn(_ optedIn: Bool) {
126+
userHasOptedIn = optedIn
127+
128+
if optedIn {
129+
refreshUserData()
130+
startObservingNotifications()
131+
DDLogInfo("🔵 Tracking started.")
132+
} else {
133+
stopObservingNotifications()
134+
analyticsProvider.clearEvents()
135+
analyticsProvider.clearUsers()
136+
DDLogInfo("🔴 Tracking opt-out complete.")
137+
}
138+
}
139+
}
140+
141+
93142
// MARK: - Private Helpers
94143
//
95144
private extension WooAnalytics {
96145

97146
func startObservingNotifications() {
147+
guard userHasOptedIn == true else {
148+
return
149+
}
150+
98151
NotificationCenter.default.addObserver(self, selector: #selector(trackApplicationOpened), name: UIApplication.didBecomeActiveNotification, object: nil)
99152
NotificationCenter.default.addObserver(self, selector: #selector(trackApplicationClosed), name: UIApplication.didEnterBackgroundNotification, object: nil)
100153
}
101154

155+
func stopObservingNotifications() {
156+
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
157+
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
158+
}
159+
102160
@objc func trackApplicationOpened() {
103161
track(.applicationOpened)
104162
applicationOpenedTime = Date()

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,17 @@ public enum WooAnalyticsStat: String {
7777

7878
// Settings View Events
7979
//
80-
case settingsTapped = "main_menu_settings_tapped"
81-
case settingsContactSupportTapped = "main_menu_contact_support_tapped"
82-
case settingsLogoutTapped = "settings_logout_button_tapped"
83-
case settingsLogoutConfirmation = "settings_logout_confirmation_dialog_result"
80+
case settingsTapped = "main_menu_settings_tapped"
81+
case settingsContactSupportTapped = "main_menu_contact_support_tapped"
82+
83+
case settingsPrivacySettingsTapped = "settings_privacy_settings_tapped"
84+
case settingsCollectInfoToggled = "settings_privacy_settings_collect_info_toggled"
85+
case settingsPrivacyPolicyTapped = "settings_privacy_settings_privacy_policy_link_tapped"
86+
case settingsShareInfoLearnMoreTapped = "settings_privacy_settings_share_info_link_tapped"
87+
case settingsThirdPartyLearnMoreTapped = "settings_privacy_settings_third_party_tracking_info_link_tapped"
88+
89+
case settingsLogoutTapped = "settings_logout_button_tapped"
90+
case settingsLogoutConfirmation = "settings_logout_confirmation_dialog_result"
8491

8592
// Order View Events
8693
//

WooCommerce/Classes/AppDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ private extension AppDelegate {
154154
///
155155
func setupFabric() {
156156
fabricManager.initialize()
157-
fabricManager.startListeningToAuthNotifications()
158157
}
159158

160159
/// Sets up the WordPress Authenticator.

WooCommerce/Classes/Extensions/UserDefaults+Woo.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ extension UserDefaults {
1010
case defaultUsername
1111
case defaultStoreID
1212
case defaultAnonymousID
13+
case userOptedInAnalytics
14+
case userOptedInCrashlytics
1315
case versionOfLastRun
1416
case analyticsUsername
1517
}

WooCommerce/Classes/System/WooConstants.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,12 @@ enum WooConstants {
2424
/// Support Email
2525
///
2626
static let supportMail = "[email protected]"
27+
28+
/// Cookie policy URL
29+
///
30+
static let cookieURL = URL(string:"https://automattic.com/cookies/")
31+
32+
/// Privacy policy URL
33+
///
34+
static let privacyURL = URL(string: "https://automattic.com/privacy/")
2735
}

WooCommerce/Classes/Tools/Fabric/FabricManager.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,37 @@ import Yosemite
1212
///
1313
class FabricManager {
1414

15+
/// Check user opt-in for Crash Reporting
16+
///
17+
var userHasOptedIn: Bool {
18+
get {
19+
let optedIn: Bool? = UserDefaults.standard.object(forKey: .userOptedInCrashlytics)
20+
return optedIn ?? true // crash reports turned on by default
21+
}
22+
set {
23+
UserDefaults.standard.set(newValue, forKey: .userOptedInCrashlytics)
24+
}
25+
}
26+
1527
deinit {
1628
NotificationCenter.default.removeObserver(self)
1729
}
1830

1931
/// Initializes the Fabric SDK.
2032
///
2133
func initialize() {
34+
startCrashlyticsIfNeeded()
35+
}
36+
37+
/// Starts Crashlytics
38+
///
39+
func startCrashlyticsIfNeeded() {
40+
guard userHasOptedIn else {
41+
return
42+
}
43+
2244
Fabric.with([Crashlytics.self])
45+
startListeningToAuthNotifications()
2346
}
2447

2548
/// Starts listening to Authentication Notifications: Fabric's metadata will be refreshed accordingly.
@@ -29,6 +52,13 @@ class FabricManager {
2952
nc.addObserver(self, selector: #selector(defaultAccountWasUpdated), name: .defaultAccountWasUpdated, object: nil)
3053
}
3154

55+
/// Stops listening to Authentication Notifications
56+
/// after tracking opt-out event
57+
func stopListeningToAuthNotifications() {
58+
let nc = NotificationCenter.default
59+
nc.removeObserver(self, name: .defaultAccountWasUpdated, object: nil)
60+
}
61+
3262
/// Handles the `.sessionWasAuthenticated` notification.
3363
///
3464
@objc func defaultAccountWasUpdated(sender: Notification) {
@@ -45,4 +75,33 @@ class FabricManager {
4575
DDLogInfo("🌡 Fabric Account Nuked!")
4676
}
4777
}
78+
79+
/// Clears Crashlytics data after opt-out of tracking event
80+
///
81+
func clearCrashlyticsParameters() {
82+
let crashlytics = Crashlytics.sharedInstance()
83+
84+
crashlytics.setUserName(nil)
85+
crashlytics.setUserEmail(nil)
86+
crashlytics.setUserIdentifier(nil)
87+
}
88+
}
89+
90+
91+
// MARK: - Tracking Opt Out
92+
//
93+
extension FabricManager {
94+
95+
func setUserHasOptedIn(_ optedIn: Bool) {
96+
userHasOptedIn = optedIn
97+
98+
if optedIn {
99+
startCrashlyticsIfNeeded()
100+
DDLogInfo("🔵 Crashlytics reporting restored.")
101+
} else {
102+
clearCrashlyticsParameters()
103+
stopListeningToAuthNotifications()
104+
DDLogInfo("🔴 Crashlytics opt-out complete.")
105+
}
106+
}
48107
}

WooCommerce/Classes/ViewRelated/Dashboard/Dashboard.storyboard

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="z1L-hy-XB6">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="z1L-hy-XB6">
33
<device id="retina4_7" orientation="portrait">
44
<adaptation id="fullscreen"/>
55
</device>
66
<dependencies>
7-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
7+
<deployment identifier="iOS"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
89
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
910
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
1011
</dependencies>
@@ -197,12 +198,47 @@
197198
</view>
198199
<connections>
199200
<outlet property="tableView" destination="wI4-XX-k2z" id="6bZ-RB-KF9"/>
201+
<segue destination="tLZ-3e-cJY" kind="show" identifier="ShowPrivacySettingsViewController" id="26d-pC-r7k"/>
200202
</connections>
201203
</viewController>
202204
<placeholder placeholderIdentifier="IBFirstResponder" id="FEk-Qx-6OE" userLabel="First Responder" sceneMemberID="firstResponder"/>
203205
</objects>
204206
<point key="canvasLocation" x="270" y="1987"/>
205207
</scene>
208+
<!--Privacy Settings View Controller-->
209+
<scene sceneID="g1b-rK-cDR">
210+
<objects>
211+
<viewController storyboardIdentifier="PrivacySettingsViewController" id="tLZ-3e-cJY" customClass="PrivacySettingsViewController" customModule="WooCommerce" customModuleProvider="target" sceneMemberID="viewController">
212+
<view key="view" contentMode="scaleToFill" id="eDL-oC-fDz">
213+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
214+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
215+
<subviews>
216+
<tableView clipsSubviews="YES" contentMode="scaleToFill" layoutMarginsFollowReadableWidth="YES" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="hwi-He-cKY">
217+
<rect key="frame" x="0.0" y="64" width="375" height="554"/>
218+
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
219+
<connections>
220+
<outlet property="dataSource" destination="tLZ-3e-cJY" id="uum-dx-Yau"/>
221+
<outlet property="delegate" destination="tLZ-3e-cJY" id="Ig0-wR-2Ms"/>
222+
</connections>
223+
</tableView>
224+
</subviews>
225+
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
226+
<constraints>
227+
<constraint firstAttribute="bottomMargin" secondItem="hwi-He-cKY" secondAttribute="bottom" id="UPd-Rr-5ev"/>
228+
<constraint firstItem="hwi-He-cKY" firstAttribute="trailing" secondItem="e4p-U3-YVf" secondAttribute="trailing" id="mcB-Fh-FAx"/>
229+
<constraint firstItem="hwi-He-cKY" firstAttribute="top" secondItem="eDL-oC-fDz" secondAttribute="topMargin" id="sVu-Pi-CPd"/>
230+
<constraint firstItem="hwi-He-cKY" firstAttribute="leading" secondItem="e4p-U3-YVf" secondAttribute="leading" id="tE5-ir-eJR"/>
231+
</constraints>
232+
<viewLayoutGuide key="safeArea" id="e4p-U3-YVf"/>
233+
</view>
234+
<connections>
235+
<outlet property="tableView" destination="hwi-He-cKY" id="dz2-gV-Lqe"/>
236+
</connections>
237+
</viewController>
238+
<placeholder placeholderIdentifier="IBFirstResponder" id="8Ut-YJ-O7J" userLabel="First Responder" sceneMemberID="firstResponder"/>
239+
</objects>
240+
<point key="canvasLocation" x="1388" y="2331.1844077961023"/>
241+
</scene>
206242
<!--Dashboard-->
207243
<scene sceneID="GIl-fY-LQR">
208244
<objects>

0 commit comments

Comments
 (0)