Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update app store prompt logic #2678

Merged
merged 7 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@
8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8563A03B1F9288D600F04442 /* BrowserChromeManager.swift */; };
8565A34B1FC8D96B00239327 /* LaunchTabNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */; };
8565A34D1FC8DFE400239327 /* LaunchTabNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */; };
857229882BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857229872BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift */; };
8572298A2BBEF0C800E2E802 /* AppRatingPrompt_v1 in Resources */ = {isa = PBXBuildFile; fileRef = 857229892BBEF0C800E2E802 /* AppRatingPrompt_v1 */; };
8577A1C5255D2C0D00D43FCD /* HitTestingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8577A1C4255D2C0D00D43FCD /* HitTestingToolbar.swift */; };
857EEB752095FFAC008A005C /* HomeRowInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857EEB742095FFAC008A005C /* HomeRowInstructionsViewController.swift */; };
858479C92B8792D800D156C1 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858479C82B8792D800D156C1 /* HistoryManager.swift */; };
Expand Down Expand Up @@ -1537,6 +1539,7 @@
8544C37A250B823600A0FE73 /* UserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserText.swift; sourceTree = "<group>"; };
8546A5492A672959003929BF /* MainViewController+Email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+Email.swift"; sourceTree = "<group>"; };
85480CB229226B1E007E8F13 /* CrashCollectionExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashCollectionExtensionTests.swift; sourceTree = "<group>"; };
85481A6A2BA46AFB00F9EFB0 /* AppRatingPrompt 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AppRatingPrompt 2.xcdatamodel"; sourceTree = "<group>"; };
85482D882462DCD100EDEDD1 /* OpenAction.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OpenAction.appex; sourceTree = BUILT_PRODUCTS_DIR; };
85482D8C2462DCD100EDEDD1 /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = "<group>"; };
85482D8F2462DCD100EDEDD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
Expand All @@ -1556,6 +1559,8 @@
8563A03B1F9288D600F04442 /* BrowserChromeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserChromeManager.swift; sourceTree = "<group>"; };
8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotification.swift; sourceTree = "<group>"; };
8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotificationTests.swift; sourceTree = "<group>"; };
857229872BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRatingPromptDatabaseMigrationTests.swift; sourceTree = "<group>"; };
857229892BBEF0C800E2E802 /* AppRatingPrompt_v1 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = AppRatingPrompt_v1; sourceTree = "<group>"; };
8577A1C4255D2C0D00D43FCD /* HitTestingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestingToolbar.swift; sourceTree = "<group>"; };
857EEB742095FFAC008A005C /* HomeRowInstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeRowInstructionsViewController.swift; sourceTree = "<group>"; };
858479C82B8792D800D156C1 /* HistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3637,8 +3642,10 @@
83134D7F20E2E013006CE65D /* Feedback */ = {
isa = PBXGroup;
children = (
857229892BBEF0C800E2E802 /* AppRatingPrompt_v1 */,
8528AE7D212EF5FF00D0BD74 /* AppRatingPromptTests.swift */,
8528AE82212FF91A00D0BD74 /* AppRatingPromptStorageTests.swift */,
857229872BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift */,
);
name = Feedback;
sourceTree = "<group>";
Expand Down Expand Up @@ -6274,6 +6281,7 @@
files = (
EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */,
F17843E91F36226700390DCD /* MockFiles in Resources */,
8572298A2BBEF0C800E2E802 /* AppRatingPrompt_v1 in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -7155,6 +7163,7 @@
B6AD9E3728D4510A0019CDE9 /* ContentBlockingUpdatingTests.swift in Sources */,
C14882E427F20D9A00D59F0C /* BookmarksImporterTests.swift in Sources */,
8588026A24E424EE00C24AB6 /* AppWidthObserverTests.swift in Sources */,
857229882BBEE74100E2E802 /* AppRatingPromptDatabaseMigrationTests.swift in Sources */,
8588026624E420BD00C24AB6 /* LargeOmniBarStateTests.swift in Sources */,
85AFA1212B45D14F0028A504 /* BookmarksMigrationAssertionTests.swift in Sources */,
EE0153EB2A6FF970002A8B26 /* NetworkProtectionRootViewModelTests.swift in Sources */,
Expand Down Expand Up @@ -10379,9 +10388,10 @@
8528AE7F212F15D600D0BD74 /* AppRatingPrompt.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
85481A6A2BA46AFB00F9EFB0 /* AppRatingPrompt 2.xcdatamodel */,
8528AE80212F15D600D0BD74 /* AppRatingPrompt.xcdatamodel */,
);
currentVersion = 8528AE80212F15D600D0BD74 /* AppRatingPrompt.xcdatamodel */;
currentVersion = 85481A6A2BA46AFB00F9EFB0 /* AppRatingPrompt 2.xcdatamodel */;
path = AppRatingPrompt.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
32 changes: 30 additions & 2 deletions DuckDuckGo/AppRatingPrompt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import CoreData

protocol AppRatingPromptStorage {

var firstShown: Date? { get set }

var lastAccess: Date? { get set }

var uniqueAccessDays: Int? { get set }
Expand All @@ -35,6 +37,10 @@ class AppRatingPrompt {

var storage: AppRatingPromptStorage

var uniqueAccessDays: Int {
storage.uniqueAccessDays ?? 0
}

init(storage: AppRatingPromptStorage = AppRatingPromptCoreDataStorage()) {
self.storage = storage
}
Expand All @@ -47,17 +53,39 @@ class AppRatingPrompt {
}

func shouldPrompt(onDate date: Date = Date()) -> Bool {
return [3, 7].contains(storage.uniqueAccessDays) && !date.isSameDay(storage.lastShown)
// To keep the database migration "lightweight" we just need to check if lastShown has been set yet.
// If it has then this user won't see any more prompts, which is preferable to seeing too many or too frequently.
if uniqueAccessDays >= 3 && storage.firstShown == nil && storage.lastShown == nil {
return true
} else if uniqueAccessDays >= 4 && storage.lastShown == nil {
return true
}
return false
}

func shown(onDate date: Date = Date()) {
storage.lastShown = date
if storage.firstShown == nil {
storage.firstShown = date
storage.uniqueAccessDays = 0
} else if storage.lastShown == nil {
storage.lastShown = date
}
}

}

class AppRatingPromptCoreDataStorage: AppRatingPromptStorage {

var firstShown: Date? {
get {
return ratingPromptEntity()?.firstShown
}
set {
ratingPromptEntity()?.firstShown = newValue
try? context.save()
}
}

var lastAccess: Date? {
get {
return ratingPromptEntity()?.lastAccess
Expand Down
8 changes: 8 additions & 0 deletions DuckDuckGo/AppRatingPrompt.xcdatamodeld/.xccurrentversion
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AppRatingPrompt 2.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23B92" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AppRatingPromptEntity" representedClassName="AppRatingPromptEntity" syncable="YES" codeGenerationType="class">
<attribute name="firstShown" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastAccess" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastShown" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uniqueAccessDays" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>
77 changes: 77 additions & 0 deletions DuckDuckGoTests/AppRatingPromptDatabaseMigrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// AppRatingPromptDatabaseMigrationTests.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import XCTest
@testable import DuckDuckGo
@testable import Core
import CoreData
import Persistence

// If making future changes take a copy of the v2 momd and Database file like here and update / add tests as appropiate.
class AppRatingPromptDatabaseMigrationTests: XCTestCase {

func testExpectedNumberOfModelVersionsInLatestModel() throws {
guard let modelURL = Bundle.main.url(forResource: "AppRatingPrompt", withExtension: "momd") else {
XCTFail("Error loading model URL")
return
}
let modelVersions = try FileManager.default.contentsOfDirectory(at: modelURL, includingPropertiesForKeys: nil, options: [])
.filter { $0.lastPathComponent.hasSuffix(".mom") }
XCTAssertEqual(2, modelVersions.count)
}

func testMigrationFromV1toLatest() {

guard let baseURL = Bundle(for: (type(of: self))).url(forResource: "AppRatingPrompt_v1", withExtension: nil) else {
XCTFail("could not get base url")
return
}

guard let latestModel = CoreDataDatabase.loadModel(from: .main, named: "AppRatingPrompt") else {
XCTFail("could not load latest model")
return
}

let storeURL = baseURL.appendingPathComponent("Database.sqlite")

do {
let options = [NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true]

// Run the migration
let newCoordinator = NSPersistentStoreCoordinator(managedObjectModel: latestModel)
let newStore = try newCoordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: options)

// Check the data exists
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = newCoordinator
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "AppRatingPromptEntity")
let count = try context.count(for: fetchRequest)
XCTAssertGreaterThan(count, 0, "Migration failed, no entities found.")

} catch {
XCTFail("Migration failed with error: \(error)")
}

}

}
Loading
Loading