Skip to content

Commit c9472e4

Browse files
Add: overlay frequency logic
1 parent c24a274 commit c9472e4

File tree

4 files changed

+260
-11
lines changed

4 files changed

+260
-11
lines changed

WordPress/Classes/ViewRelated/Jetpack/Branding/Coordinator/JetpackFeaturesRemovalCoordinator.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ class JetpackFeaturesRemovalCoordinator {
1111
case three
1212
case four
1313
case newUsers
14+
15+
var frequencyConfig: JetpackOverlayFrequencyTracker.FrequencyConfig {
16+
switch self {
17+
case .one:
18+
fallthrough
19+
case .two:
20+
return .init(featureSpecificInDays: 7, generalInDays: 2)
21+
case .three:
22+
return .init(featureSpecificInDays: 4, generalInDays: 1)
23+
default:
24+
return .defaultConfig
25+
}
26+
}
1427
}
1528

1629
/// Enum descibing the current phase of the site creation flow removal
@@ -77,8 +90,9 @@ class JetpackFeaturesRemovalCoordinator {
7790
/// - viewController: View controller where the overlay should be presented in.
7891
static func presentOverlay(from source: OverlaySource, in viewController: UIViewController) {
7992
let phase = generalPhase()
93+
let frequencyConfig = phase.frequencyConfig
8094
let config = JetpackFullscreenOverlayGeneralConfig(phase: phase, source: source)
81-
let frequencyTracker = JetpackOverlayFrequencyTracker(phase: phase, source: source)
95+
let frequencyTracker = JetpackOverlayFrequencyTracker(frequencyConfig: frequencyConfig, source: source)
8296
guard config.shouldShowOverlay, frequencyTracker.shouldShow() else {
8397
return
8498
}
Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,101 @@
11
import Foundation
22

3-
struct JetpackOverlayFrequencyTracker {
4-
let phase: JetpackFeaturesRemovalCoordinator.GeneralPhase // TODO: Do we need this?
5-
let source: JetpackFeaturesRemovalCoordinator.OverlaySource
3+
class JetpackOverlayFrequencyTracker {
4+
5+
private let frequencyConfig: FrequencyConfig
6+
private let source: JetpackFeaturesRemovalCoordinator.OverlaySource
7+
private let persistenceStore: UserPersistentRepository
8+
9+
private var lastSavedGenericDate: Date? {
10+
get {
11+
let key = Constants.lastDateKeyPrefix
12+
return persistenceStore.object(forKey: key) as? Date
13+
}
14+
set {
15+
let key = Constants.lastDateKeyPrefix
16+
persistenceStore.set(newValue, forKey: key)
17+
}
18+
}
19+
20+
private var lastSavedSourceDate: Date? {
21+
get {
22+
let sourceKey = "\(Constants.lastDateKeyPrefix)-\(source.rawValue)"
23+
return persistenceStore.object(forKey: sourceKey) as? Date
24+
}
25+
set {
26+
let sourceKey = "\(Constants.lastDateKeyPrefix)-\(source.rawValue)"
27+
persistenceStore.set(newValue, forKey: sourceKey)
28+
}
29+
}
30+
31+
init(frequencyConfig: FrequencyConfig = .defaultConfig,
32+
source: JetpackFeaturesRemovalCoordinator.OverlaySource,
33+
persistenceStore: UserPersistentRepository = UserDefaults.standard) {
34+
self.frequencyConfig = frequencyConfig
35+
self.source = source
36+
self.persistenceStore = persistenceStore
37+
}
638

739
func shouldShow() -> Bool {
8-
// TODO: To be implemented
9-
// Show once for login and app open
10-
// Always show for card
11-
// Check frequency for features
12-
return true
40+
guard let lastSavedGenericDate = lastSavedGenericDate,
41+
let lastSavedSourceDate = lastSavedSourceDate else {
42+
return true
43+
}
44+
45+
switch source {
46+
case .stats:
47+
fallthrough
48+
case .notifications:
49+
fallthrough
50+
case .reader:
51+
// Check frequencies for features
52+
return frequenciesPassed(lastSavedGenericDate: lastSavedGenericDate,
53+
lastSavedSourceDate: lastSavedSourceDate)
54+
case .card:
55+
return true // Always show for card
56+
case .login:
57+
fallthrough
58+
case .appOpen:
59+
return false // Show once for login and app open
60+
}
1361
}
1462

1563
func track() {
16-
// TODO: To be implemented
17-
// record that the overlay was displayed
64+
let date = Date()
65+
lastSavedSourceDate = date
66+
lastSavedGenericDate = date
67+
}
68+
69+
private func frequenciesPassed(lastSavedGenericDate: Date, lastSavedSourceDate: Date) -> Bool {
70+
let secondsSinceLastSavedSourceDate = lastSavedSourceDate.timeIntervalSinceNow
71+
let secondsSinceLastSavedGenericDate = lastSavedGenericDate.timeIntervalSinceNow
72+
let featureSpecificFreqPassed = secondsSinceLastSavedSourceDate > frequencyConfig.featureSpecificInSeconds
73+
let generalFreqPassed = secondsSinceLastSavedGenericDate > frequencyConfig.generalInSeconds
74+
return generalFreqPassed && featureSpecificFreqPassed
75+
}
76+
}
77+
78+
extension JetpackOverlayFrequencyTracker {
79+
struct FrequencyConfig {
80+
// MARK: Instance Variables
81+
let featureSpecificInDays: Int
82+
let generalInDays: Int
83+
84+
// MARK: Static Variables
85+
static let defaultConfig = FrequencyConfig(featureSpecificInDays: 0, generalInDays: 0)
86+
private static let secondsInDay: TimeInterval = 86_400
87+
88+
// MARK: Computed Variables
89+
var featureSpecificInSeconds: TimeInterval {
90+
return TimeInterval(featureSpecificInDays) * Self.secondsInDay
91+
}
92+
93+
var generalInSeconds: TimeInterval {
94+
return TimeInterval(generalInDays) * Self.secondsInDay
95+
}
96+
}
97+
98+
enum Constants {
99+
static let lastDateKeyPrefix = "JetpackOverlayLastDate"
18100
}
19101
}

WordPress/WordPress.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,7 @@
15161516
801D9517291AB3CF0051993E /* JetpackNotificationsLogoAnimation_ltr.json in Resources */ = {isa = PBXBuildFile; fileRef = 801D950C291AB3CF0051993E /* JetpackNotificationsLogoAnimation_ltr.json */; };
15171517
801D951A291AC0B00051993E /* JetpackOverlayFrequencyTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D9519291AC0B00051993E /* JetpackOverlayFrequencyTracker.swift */; };
15181518
801D951B291AC0B00051993E /* JetpackOverlayFrequencyTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D9519291AC0B00051993E /* JetpackOverlayFrequencyTracker.swift */; };
1519+
801D951D291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 801D951C291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift */; };
15191520
803C493B283A7C0C00003E9B /* QuickStartChecklistHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803C493A283A7C0C00003E9B /* QuickStartChecklistHeader.swift */; };
15201521
803C493C283A7C0C00003E9B /* QuickStartChecklistHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803C493A283A7C0C00003E9B /* QuickStartChecklistHeader.swift */; };
15211522
803C493E283A7C2200003E9B /* QuickStartChecklistHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 803C493D283A7C2200003E9B /* QuickStartChecklistHeader.xib */; };
@@ -6759,6 +6760,7 @@
67596760
801D950B291AB3CE0051993E /* JetpackNotificationsLogoAnimation_rtl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackNotificationsLogoAnimation_rtl.json; sourceTree = "<group>"; };
67606761
801D950C291AB3CF0051993E /* JetpackNotificationsLogoAnimation_ltr.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JetpackNotificationsLogoAnimation_ltr.json; sourceTree = "<group>"; };
67616762
801D9519291AC0B00051993E /* JetpackOverlayFrequencyTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackOverlayFrequencyTracker.swift; sourceTree = "<group>"; };
6763+
801D951C291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackOverlayFrequencyTrackerTests.swift; sourceTree = "<group>"; };
67626764
80293CF6284450AD0083F946 /* WordPress-Swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WordPress-Swift.h"; sourceTree = "<group>"; };
67636765
803C493A283A7C0C00003E9B /* QuickStartChecklistHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickStartChecklistHeader.swift; sourceTree = "<group>"; };
67646766
803C493D283A7C2200003E9B /* QuickStartChecklistHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickStartChecklistHeader.xib; sourceTree = "<group>"; };
@@ -12281,6 +12283,7 @@
1228112283
isa = PBXGroup;
1228212284
children = (
1228312285
803DE81E290636A4007D4E9C /* JetpackFeaturesRemovalCoordinatorTests.swift */,
12286+
801D951C291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift */,
1228412287
);
1228512288
name = Jetpack;
1228612289
sourceTree = "<group>";
@@ -21890,6 +21893,7 @@
2189021893
8BC12F7723201B86004DDA72 /* PostService+MarkAsFailedAndDraftIfNeededTests.swift in Sources */,
2189121894
803DE81F290636A4007D4E9C /* JetpackFeaturesRemovalCoordinatorTests.swift in Sources */,
2189221895
937250EE267A492D0086075F /* StatsPeriodStoreTests.swift in Sources */,
21896+
801D951D291ADB7E0051993E /* JetpackOverlayFrequencyTrackerTests.swift in Sources */,
2189321897
F1B1E7A324098FA100549E2A /* BlogTests.swift in Sources */,
2189421898
57889AB823589DF100DAE56D /* PageBuilder.swift in Sources */,
2189521899
3FDDFE9627C8178C00606933 /* SiteStatsInformationTests.swift in Sources */,
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import XCTest
2+
@testable import WordPress
3+
4+
final class JetpackOverlayFrequencyTrackerTests: XCTestCase {
5+
6+
private enum Constants {
7+
static let frequencyConfig = JetpackOverlayFrequencyTracker.FrequencyConfig(featureSpecificInDays: 4, generalInDays: 2)
8+
static let oneDayInSeconds: TimeInterval = 86_400
9+
static let threeDaysInSeconds: TimeInterval = 259_200
10+
static let fiveDaysInSeconds: TimeInterval = 432_000
11+
}
12+
13+
private var mockUserDefaults: InMemoryUserDefaults!
14+
15+
override func setUp() {
16+
mockUserDefaults = InMemoryUserDefaults()
17+
}
18+
19+
func testTrackingOverlay() throws {
20+
// Given
21+
let key = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-stats"
22+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
23+
let tracker = JetpackOverlayFrequencyTracker(source: .stats, persistenceStore: mockUserDefaults)
24+
25+
// When
26+
tracker.track()
27+
28+
// Then
29+
let savedDate = try XCTUnwrap(mockUserDefaults.object(forKey: key) as? Date)
30+
let savedGenericDate = try XCTUnwrap(mockUserDefaults.object(forKey: genericKey) as? Date)
31+
let savedDateInSeconds = savedDate.timeIntervalSince1970
32+
let savedGenericDateInSeconds = savedGenericDate.timeIntervalSince1970
33+
let nowInSeconds = Date().timeIntervalSince1970
34+
XCTAssertEqual(savedDateInSeconds, nowInSeconds, accuracy: 10)
35+
XCTAssertEqual(savedGenericDateInSeconds, nowInSeconds, accuracy: 10)
36+
}
37+
38+
func testAlwaysShowCardOverlays() {
39+
// Given
40+
let key = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-card"
41+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
42+
let tracker = JetpackOverlayFrequencyTracker(source: .card, persistenceStore: mockUserDefaults)
43+
mockUserDefaults.set(Date(), forKey: key)
44+
mockUserDefaults.set(Date(), forKey: genericKey)
45+
46+
// When & Then
47+
XCTAssertTrue(tracker.shouldShow())
48+
}
49+
50+
func testShowLoginOverlayOnlyOnce() {
51+
// Given
52+
let key = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-login"
53+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
54+
let tracker = JetpackOverlayFrequencyTracker(source: .login, persistenceStore: mockUserDefaults)
55+
56+
// When & Then
57+
XCTAssertTrue(tracker.shouldShow())
58+
59+
// Given
60+
let distantDate = Date.distantPast
61+
mockUserDefaults.set(distantDate, forKey: key)
62+
mockUserDefaults.set(distantDate, forKey: genericKey)
63+
64+
// When & Then
65+
XCTAssertFalse(tracker.shouldShow())
66+
}
67+
68+
func testShowAppOpenOverlayOnlyOnce() {
69+
// Given
70+
let key = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-app_open"
71+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
72+
let tracker = JetpackOverlayFrequencyTracker(source: .appOpen, persistenceStore: mockUserDefaults)
73+
74+
// When & Then
75+
XCTAssertTrue(tracker.shouldShow())
76+
77+
// Given
78+
let distantDate = Date.distantPast
79+
mockUserDefaults.set(distantDate, forKey: key)
80+
mockUserDefaults.set(distantDate, forKey: genericKey)
81+
82+
// When & Then
83+
XCTAssertFalse(tracker.shouldShow())
84+
}
85+
86+
func testFeatureSpecificFrequency() {
87+
// Given
88+
let statsKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-stats"
89+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
90+
let tracker = JetpackOverlayFrequencyTracker(frequencyConfig: Constants.frequencyConfig,
91+
source: .stats,
92+
persistenceStore: mockUserDefaults)
93+
94+
// When & Then
95+
XCTAssertTrue(tracker.shouldShow()) // First time
96+
97+
// Given
98+
let threeDaysAgo = Date(timeInterval: Constants.threeDaysInSeconds, since: Date())
99+
mockUserDefaults.set(threeDaysAgo, forKey: statsKey)
100+
mockUserDefaults.set(threeDaysAgo, forKey: genericKey)
101+
102+
// When & Then
103+
XCTAssertFalse(tracker.shouldShow()) // Before feature-specific frequency have passed
104+
105+
// Given
106+
let fiveDaysAgo = Date(timeInterval: Constants.fiveDaysInSeconds, since: Date())
107+
mockUserDefaults.set(fiveDaysAgo, forKey: statsKey)
108+
mockUserDefaults.set(fiveDaysAgo, forKey: genericKey)
109+
110+
// When & Then
111+
XCTAssertTrue(tracker.shouldShow()) // After feature-specific frequency have passed
112+
}
113+
114+
func testGeneralFrequency() {
115+
// Given
116+
let statsKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix + "-stats"
117+
let genericKey = JetpackOverlayFrequencyTracker.Constants.lastDateKeyPrefix
118+
let statsTracker = JetpackOverlayFrequencyTracker(frequencyConfig: Constants.frequencyConfig,
119+
source: .stats,
120+
persistenceStore: mockUserDefaults)
121+
let readerTracker = JetpackOverlayFrequencyTracker(frequencyConfig: Constants.frequencyConfig,
122+
source: .reader,
123+
persistenceStore: mockUserDefaults)
124+
125+
// When & Then
126+
XCTAssertTrue(statsTracker.shouldShow()) // First time
127+
XCTAssertTrue(readerTracker.shouldShow()) // First time
128+
129+
// Given
130+
let oneDayAgo = Date(timeInterval: Constants.oneDayInSeconds, since: Date())
131+
mockUserDefaults.set(oneDayAgo, forKey: statsKey)
132+
mockUserDefaults.set(oneDayAgo, forKey: genericKey)
133+
134+
// When & Then
135+
XCTAssertFalse(statsTracker.shouldShow()) // Before generic frequency have passed
136+
XCTAssertFalse(statsTracker.shouldShow()) // Before generic frequency have passed
137+
138+
// Given
139+
let threeDaysAgo = Date(timeInterval: Constants.threeDaysInSeconds, since: Date())
140+
mockUserDefaults.set(threeDaysAgo, forKey: statsKey)
141+
mockUserDefaults.set(threeDaysAgo, forKey: genericKey)
142+
143+
// When & Then
144+
XCTAssertFalse(statsTracker.shouldShow()) // Before feature-specific frequency have passed
145+
XCTAssertTrue(readerTracker.shouldShow()) // After generic frequency have passed
146+
}
147+
148+
149+
}

0 commit comments

Comments
 (0)