Skip to content

Commit

Permalink
Merge release/v1.2.0 into master
Browse files Browse the repository at this point in the history
  • Loading branch information
simonmitchell committed Mar 28, 2020
2 parents 0be1a83 + 4690fa9 commit a770835
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 66 deletions.
2 changes: 1 addition & 1 deletion Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<string>1.2.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))
]
```

Expand Down Expand Up @@ -103,6 +103,27 @@ 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 |

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

### Score
Expand Down Expand Up @@ -172,4 +193,4 @@ If your platform doesn't support `SKStoreReviewController` you can provide a cus

```swift
requestController.reviewRequester = myCustomRequester
```
```
16 changes: 8 additions & 8 deletions ReviewKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
183 changes: 130 additions & 53 deletions Sources/ReviewKit/ReviewRequestController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,135 @@ 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. 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()
}

/// 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. If no previous sessions have occured, this will return false
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
}

/// Returns whether the current session's score is above the threshold to show a review
public var currentSessionIsAboveScoreThreshold: Bool {
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)
Expand Down Expand Up @@ -261,63 +390,11 @@ 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 _currentSession.score > scoreThreshold else {
callback?(Result.success(false))
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 allReviewPromptCriteriaSatisfied(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
}
}

// Test based on version change since last review
if let lastRequestVersion = storage.lastRequestVersion, let versionTimeout = reviewVersionTimeout, _currentSession.version - lastRequestVersion < versionTimeout {
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)) {
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 let reviewRequester = reviewRequester else {
callback?(Result.failure(RequestError.reviewRequesterNotProvided))
return
Expand Down

0 comments on commit a770835

Please sign in to comment.