From a4740394a2b6473f4bf6eb61f8a1403a9773047c Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 10:24:25 +0000 Subject: [PATCH 1/6] Separates out review checks into independent functions and publicises variables to access them from outside the library! --- .../ReviewKit/ReviewRequestController.swift | 110 ++++++++++++++---- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/Sources/ReviewKit/ReviewRequestController.swift b/Sources/ReviewKit/ReviewRequestController.swift index 8fa1f5c..ddbf870 100644 --- a/Sources/ReviewKit/ReviewRequestController.swift +++ b/Sources/ReviewKit/ReviewRequestController.swift @@ -233,6 +233,83 @@ public final class ReviewRequestController { storage.numberOfSessions += 1 } + /// Returns whether the initial timeout for the first prompt to be shown has elapsed + /// - Parameter date: The date to check against, this allows for testing of this function + private func timeoutSinceFirstSessionHasElapsed(for date: Date = Date()) -> Bool { + // Make sure we meet the minimum timeout for showing this to the user + let firstSessionDate = storage.firstSessionDate ?? _currentSession.date + let numberOfSessions = storage.numberOfSessions + return initialRequestTimeout.hasElapsedFor(sessions: numberOfSessions, duration: date.timeIntervalSince(firstSessionDate)) + } + + /// Returns whether the initial timeout for the first prompt to be shown has elapsed + public var timeoutSinceFirstSessionHasElapsed: Bool { + return timeoutSinceFirstSessionHasElapsed() + } + + /// Returns whether the timeout since the last review prompt was shown has elapsed + /// - Parameter date: The date to check against, this allows for testing of this function + private func timeoutSinceLastRequestHasElapsed(for date: Date = Date()) -> Bool { + // Make sure we meet the minimum timeout for showing this to the user + guard let lastRequestDate = storage.lastRequestDate, let lastRequestSession = storage.lastRequestSession else { + return true + } + let numberOfSessions = storage.numberOfSessions + return reviewRequestTimeout.hasElapsedFor(sessions: numberOfSessions - lastRequestSession, duration: date.timeIntervalSince(lastRequestDate)) + } + + /// Returns whether the initial timeout for the first prompt to be shown has elapsed + public var timeoutSinceLastRequestHasElapsed: Bool { + return timeoutSinceLastRequestHasElapsed() + } + + /// Returns whether the bad request timeout has elapsed + /// - Parameter date: The date to check against, this allows for testing of this function + private func timeoutSinceLastBadSessionHasElapsed(for date: Date = Date()) -> Bool { + // Get all sessions exluding the current session + let sessions = storage.sessions.filter({ $0 != _currentSession }) + + // Get the last bad session + guard let lastBadSessionElement = sessions.enumerated().map({ $0 }).last(where: { $0.element.isBad }) else { + return true + } + + let sessionsDiff = storage.sessions.count - lastBadSessionElement.offset + let timeSince = date.timeIntervalSince(lastBadSessionElement.element.date) + return badSessionTimeout.hasElapsedFor(sessions: sessionsDiff, duration: timeSince) + } + + /// Returns whether the bad request timeout has elapsed + /// - Parameter date: The date to check against, this allows for testing of this function + public var timeoutSinceLastBadSessionHasElapsed: Bool { + return timeoutSinceLastBadSessionHasElapsed() + } + + /// Returns whether the app version has changed significantly enough since the last review prompt + public var versionChangeSinceLastRequestIsSatisfied: Bool { + guard let lastRequestVersion = storage.lastRequestVersion, let versionTimeout = reviewVersionTimeout else { + return true + } + return _currentSession.version - lastRequestVersion >= versionTimeout + } + + /// Returns whether the average score threshold has been met over the last n sessions + public var averageScoreThresholdIsMet: Bool { + + // Get all sessions exluding the current session + let sessions = storage.sessions.filter({ $0 != _currentSession }) + guard averageScoreThreshold.sessions > 0 else { + return true + } + guard !sessions.isEmpty else { + return false + } + + let sessionsForAverage = sessions.suffix(averageScoreThreshold.sessions) + let averageScore = sessionsForAverage.map({ $0.score }).average + return averageScore >= averageScoreThreshold.score + } + /// Logs a given app action /// - Parameter action: The action that occured /// - Parameter callback: A callback which lets you know if a review was requested (Or possibly requested in the case of SKStoreReviewController) @@ -273,49 +350,34 @@ public final class ReviewRequestController { return } - // Make sure we meet the minimum timeout for showing this to the user - let firstSessionDate = storage.firstSessionDate ?? _currentSession.date - let numberOfSessions = storage.numberOfSessions - // Test based on initial timeout - guard initialRequestTimeout.hasElapsedFor(sessions: numberOfSessions, duration: currentDate.timeIntervalSince(firstSessionDate)) else { + guard timeoutSinceFirstSessionHasElapsed(for: currentDate) else { callback?(Result.success(false)) return } - // Get all sessions exluding the current session - let sessions = storage.sessions.filter({ $0 != _currentSession }) - // Make sure timeout since last bad session has elapsed! - if let lastBadSessionElement = sessions.enumerated().map({ $0 }).last(where: { $0.element.isBad }) { - let sessionsDiff = storage.sessions.count - lastBadSessionElement.offset - let timeSince = currentDate.timeIntervalSince(lastBadSessionElement.element.date) - if !badSessionTimeout.hasElapsedFor(sessions: sessionsDiff, duration: timeSince) { - callback?(Result.success(false)) - return - } + guard timeoutSinceLastBadSessionHasElapsed(for: currentDate) else { + callback?(Result.success(false)) + return } // Test based on version change since last review - if let lastRequestVersion = storage.lastRequestVersion, let versionTimeout = reviewVersionTimeout, _currentSession.version - lastRequestVersion < versionTimeout { + guard versionChangeSinceLastRequestIsSatisfied else { callback?(Result.success(false)) return } // Test based on timeout since last review - if let lastRequestDate = storage.lastRequestDate, let lastRequestSession = storage.lastRequestSession, !reviewRequestTimeout.hasElapsedFor(sessions: numberOfSessions - lastRequestSession, duration: currentDate.timeIntervalSince(lastRequestDate)) { + guard timeoutSinceLastRequestHasElapsed(for: currentDate) else { callback?(Result.success(false)) return } // Make sure average score is met! - if averageScoreThreshold.sessions > 0, !sessions.isEmpty { - let sessionsForAverage = sessions.suffix(averageScoreThreshold.sessions) - let averageScore = sessionsForAverage.map({ $0.score }).average - if averageScore < averageScoreThreshold.score { - callback?(Result.success(false)) - return - } + guard averageScoreThresholdIsMet else { + callback?(Result.success(false)) + return } guard let reviewRequester = reviewRequester else { From 16b63d2a18cccc7c81ae5a4cc76c48887016cae5 Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 13:10:35 +0000 Subject: [PATCH 2/6] Adds new property and updates docs --- README.md | 15 +++++++++++++++ Sources/ReviewKit/ReviewRequestController.swift | 11 ++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3bb21ba..b28efb1 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,21 @@ You can control the review prompt that is shown by implementing the `ReviewReque ⚠️ **If you are using this library on an operating system which doesn't support `SKStoreReviewController` you MUST provide a value for this property.** ⚠️ +###Accessing Checks + +The same checks that the library does internally can be accessed individually through a set of properties. These could be useful for example if you have a manually + +| Property | Description | +|---|---| +| timeoutSinceFirstSessionHasElapsed | Whether the timeout since the first app session has elapsed | +| timeoutSinceLastRequestHasElapsed | Whether the timeout since the last time the user was prompted for a review has passed | +| timeoutSinceLastBadSessionHasElapsed | Whether the timout since the last time the user experienced a 'bad' session has passed | +| versionChangeSinceLastRequestIsSatisfied | Whether the required app version change has occured since the last version the user was prompted for a review on | +| averageScoreThresholdIsMet | Whether the average score over the last 'n' sessions has been met. If no previous sessions are recorded, this will return `false` | +| currentSessionIsAboveScoreThreshold | Whether the current session has met the required score threshold | + +The values returned in these variables are all based on thee configuration settings below. + ## Configuration ### Score diff --git a/Sources/ReviewKit/ReviewRequestController.swift b/Sources/ReviewKit/ReviewRequestController.swift index ddbf870..4b5cdb2 100644 --- a/Sources/ReviewKit/ReviewRequestController.swift +++ b/Sources/ReviewKit/ReviewRequestController.swift @@ -279,7 +279,7 @@ public final class ReviewRequestController { return badSessionTimeout.hasElapsedFor(sessions: sessionsDiff, duration: timeSince) } - /// Returns whether the bad request timeout has elapsed + /// Returns whether the bad request timeout has elapsed. This ignores the current session, which should be checked separately. /// - Parameter date: The date to check against, this allows for testing of this function public var timeoutSinceLastBadSessionHasElapsed: Bool { return timeoutSinceLastBadSessionHasElapsed() @@ -293,7 +293,7 @@ public final class ReviewRequestController { return _currentSession.version - lastRequestVersion >= versionTimeout } - /// Returns whether the average score threshold has been met over the last n sessions + /// Returns whether the average score threshold has been met over the last n sessions. If no previous sessions have occured, this will return false public var averageScoreThresholdIsMet: Bool { // Get all sessions exluding the current session @@ -310,6 +310,11 @@ public final class ReviewRequestController { return averageScore >= averageScoreThreshold.score } + /// Returns whether the current session's score is above the threshold to show a review + public var currentSessionIsAboveScoreThreshold: Bool { + return _currentSession.score > scoreThreshold + } + /// Logs a given app action /// - Parameter action: The action that occured /// - Parameter callback: A callback which lets you know if a review was requested (Or possibly requested in the case of SKStoreReviewController) @@ -345,7 +350,7 @@ public final class ReviewRequestController { } // Make sure we have met the score threshold for this session - guard _currentSession.score > scoreThreshold else { + guard currentSessionIsAboveScoreThreshold else { callback?(Result.success(false)) return } From 61be99066cdabdb84b28ebc22a3af6c63fb3a8e5 Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 17:27:48 +0000 Subject: [PATCH 3/6] Adds `allReviewPromptCriteriaSatisfied` --- README.md | 6 ++ .../ReviewKit/ReviewRequestController.swift | 86 +++++++++++-------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b28efb1..f14ec87 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,12 @@ The same checks that the library does internally can be accessed individually th | averageScoreThresholdIsMet | Whether the average score over the last 'n' sessions has been met. If no previous sessions are recorded, this will return `false` | | currentSessionIsAboveScoreThreshold | Whether the current session has met the required score threshold | +All of these can be checked in one go by checking + +```swift +ReviewRequestController.shared.allReviewPromptCriteriaSatisfied +``` + The values returned in these variables are all based on thee configuration settings below. ## Configuration diff --git a/Sources/ReviewKit/ReviewRequestController.swift b/Sources/ReviewKit/ReviewRequestController.swift index 4b5cdb2..43ec5a4 100644 --- a/Sources/ReviewKit/ReviewRequestController.swift +++ b/Sources/ReviewKit/ReviewRequestController.swift @@ -315,6 +315,53 @@ public final class ReviewRequestController { return _currentSession.score > scoreThreshold } + /// Returns whether all review prompt criteria have been satisfied in the current session for the given date + /// - Parameter date: The date to check all criteria against + /// - Returns: Whether all criteria have been satisfied + private func allReviewPromptCriteriaSatisfied(for date: Date = Date()) -> Bool { + // Make sure if the session has been marked as bad, then it's ignored if that option is enabled + guard !_currentSession.isBad || !disabledForBadSession else { + return false + } + + // Make sure we have met the score threshold for this session + guard currentSessionIsAboveScoreThreshold else { + return false + } + + // Test based on initial timeout + guard timeoutSinceFirstSessionHasElapsed(for: date) else { + return false + } + + // Make sure timeout since last bad session has elapsed! + guard timeoutSinceLastBadSessionHasElapsed(for: date) else { + return false + } + + // Test based on version change since last review + guard versionChangeSinceLastRequestIsSatisfied else { + return false + } + + // Test based on timeout since last review + guard timeoutSinceLastRequestHasElapsed(for: date) else { + return false + } + + // Make sure average score is met! + guard averageScoreThresholdIsMet else { + return false + } + + return true + } + + /// Returns whether all review prompt criteria have been satisfied in the current session + public var allReviewPromptCriteriaSatisfied: Bool { + return allReviewPromptCriteriaSatisfied() + } + /// Logs a given app action /// - Parameter action: The action that occured /// - Parameter callback: A callback which lets you know if a review was requested (Or possibly requested in the case of SKStoreReviewController) @@ -343,44 +390,7 @@ public final class ReviewRequestController { return } - // Make sure if the session has been marked as bad, then it's ignored if that option is enabled - guard !_currentSession.isBad || !disabledForBadSession else { - callback?(Result.success(false)) - return - } - - // Make sure we have met the score threshold for this session - guard currentSessionIsAboveScoreThreshold else { - callback?(Result.success(false)) - return - } - - // Test based on initial timeout - guard timeoutSinceFirstSessionHasElapsed(for: currentDate) else { - callback?(Result.success(false)) - return - } - - // Make sure timeout since last bad session has elapsed! - guard timeoutSinceLastBadSessionHasElapsed(for: currentDate) else { - callback?(Result.success(false)) - return - } - - // Test based on version change since last review - guard versionChangeSinceLastRequestIsSatisfied else { - callback?(Result.success(false)) - return - } - - // Test based on timeout since last review - guard timeoutSinceLastRequestHasElapsed(for: currentDate) else { - callback?(Result.success(false)) - return - } - - // Make sure average score is met! - guard averageScoreThresholdIsMet else { + guard reviewPromptCriteriaSatisfied(for: currentDate) else { callback?(Result.success(false)) return } From 645cad068e34c195201c1bcbf746d28864af31e0 Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 17:45:11 +0000 Subject: [PATCH 4/6] Fixes compiler warning after temporary rename --- Sources/ReviewKit/ReviewRequestController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ReviewKit/ReviewRequestController.swift b/Sources/ReviewKit/ReviewRequestController.swift index 43ec5a4..6839b82 100644 --- a/Sources/ReviewKit/ReviewRequestController.swift +++ b/Sources/ReviewKit/ReviewRequestController.swift @@ -390,7 +390,7 @@ public final class ReviewRequestController { return } - guard reviewPromptCriteriaSatisfied(for: currentDate) else { + guard allReviewPromptCriteriaSatisfied(for: currentDate) else { callback?(Result.success(false)) return } From 400e57812873ccfe9c272ad68d663279893cfce1 Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 17:29:10 +0000 Subject: [PATCH 5/6] Updates MARKETING_VERSION --- ReviewKit.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ReviewKit.xcodeproj/project.pbxproj b/ReviewKit.xcodeproj/project.pbxproj index 8b10eb7..47ae270 100644 --- a/ReviewKit.xcodeproj/project.pbxproj +++ b/ReviewKit.xcodeproj/project.pbxproj @@ -449,7 +449,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-iOS"; PRODUCT_NAME = ReviewKit; SKIP_INSTALL = YES; @@ -478,7 +478,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-iOS"; PRODUCT_NAME = ReviewKit; SKIP_INSTALL = YES; @@ -506,7 +506,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-watchOS"; PRODUCT_NAME = ReviewKit; SDKROOT = watchos; @@ -537,7 +537,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-watchOS"; PRODUCT_NAME = ReviewKit; SDKROOT = watchos; @@ -566,7 +566,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-tvOS"; PRODUCT_NAME = ReviewKit; SDKROOT = appletvos; @@ -596,7 +596,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-tvOS"; PRODUCT_NAME = ReviewKit; SDKROOT = appletvos; @@ -627,7 +627,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-macOS"; PRODUCT_NAME = ReviewKit; SDKROOT = macosx; @@ -657,7 +657,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.yellowbrickbear.ReviewKit-macOS"; PRODUCT_NAME = ReviewKit; SDKROOT = macosx; From 4690fa9ea3c3546a483e86a8bce14a2c5577b44c Mon Sep 17 00:00:00 2001 From: Simon Mitchell Date: Sat, 28 Mar 2020 17:33:14 +0000 Subject: [PATCH 6/6] Bumps versions in README --- Info.plist | 2 +- Package.swift | 2 +- README.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Info.plist b/Info.plist index 7c5be14..bfaf103 100644 --- a/Info.plist +++ b/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.0 + 1.2.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/Package.swift b/Package.swift index 522d222..ff2441a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index f14ec87..d3e0d98 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Usage on other platforms is outlined [below](#other-platforms) [Carthage](https://github.com/Carthage/Carthage) is a package manager which either builds projects and provides you with binaries or uses pre-built frameworks from release tags in GitHub. To add ReviewKit to your project, simply specify it in your `Cartfile`: ```ogdl -github "simonmitchell/ReviewKit" ~> 1.1.0 +github "simonmitchell/ReviewKit" ~> 1.2.0 ``` ### Swift Package Manager @@ -42,7 +42,7 @@ To add ReviewKit to your project simply add it to your dependencies array: ```swift dependencies: [ - .package(url: "https://github.com/simonmitchell/ReviewKit.git", .upToNextMajor(from: "1.1.0")) + .package(url: "https://github.com/simonmitchell/ReviewKit.git", .upToNextMajor(from: "1.2.0")) ] ``` @@ -193,4 +193,4 @@ If your platform doesn't support `SKStoreReviewController` you can provide a cus ```swift requestController.reviewRequester = myCustomRequester -``` \ No newline at end of file +```