Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 6 additions & 4 deletions Nightcap/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct AppFeature {
@Dependency(\.powerAssertionClient) var assertion
@Dependency(\.launchAtLoginClient) var launchAtLogin
@Dependency(\.appQuitterClient) var quitter
@Dependency(\.reviewPromptClient) var reviewPrompt

var body: some ReducerOf<Self> {
Reduce { state, action in
Expand Down Expand Up @@ -75,14 +76,15 @@ struct AppFeature {
return .none

case let .addAppRequested(app):
if !state.watchedApps.contains(where: { $0.bundleID == app.bundleID }) {
state.$watchedApps.withLock { $0.append(app) }
}
guard !state.watchedApps.contains(where: { $0.bundleID == app.bundleID }) else { return .none }
state.$watchedApps.withLock { $0.append(app) }
if lifecycle.runningBundleIDs().contains(app.bundleID) {
state.runningWatchedIDs.insert(app.bundleID)
syncAssertion(&state)
}
return .none
return .run { _ in
reviewPrompt.requestIfAppropriate()
}

case let .removeAppRequested(id):
state.$watchedApps.withLock { $0.removeAll { $0.bundleID == id } }
Expand Down
34 changes: 34 additions & 0 deletions Nightcap/Services/ReviewPromptClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Dependencies
import DependenciesMacros
import Foundation
import StoreKit

@DependencyClient
struct ReviewPromptClient: Sendable {
var requestIfAppropriate: @Sendable () -> Void
}

extension ReviewPromptClient: DependencyKey {
static let liveValue: ReviewPromptClient = .init(
requestIfAppropriate: {
let defaults = UserDefaults.standard
let key = "reviewPrompt.lastRequestDate"
let now = Date()
if let last = defaults.object(forKey: key) as? Date,
now.timeIntervalSince(last) < 60 * 60 * 24 * 30 {
return
}
defaults.set(now, forKey: key)
SKStoreReviewController.requestReview()
}
)

static let testValue = ReviewPromptClient(requestIfAppropriate: {})
}

extension DependencyValues {
var reviewPromptClient: ReviewPromptClient {
get { self[ReviewPromptClient.self] }
set { self[ReviewPromptClient.self] = newValue }
}
}
24 changes: 23 additions & 1 deletion NightcapTests/NightcapAppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ final class NightcapAppTests: XCTestCase {

let duplicate = WatchedApp(bundleID: "com.mitchellh.ghostty", displayName: "Ghostty")
await store.send(.addAppRequested(duplicate))
XCTAssertEqual(env.reviewPrompts.value, 0)
}

func test_add_new_app_requests_review_after_value_moment() async {
let env = makeEnv(running: [])
let store = makeStore(env: env)

await store.send(.onAppear) {
$0.launchAtLoginStatus = .disabled
}

let app = WatchedApp(bundleID: "com.example.writer", displayName: "Writer")
await store.send(.addAppRequested(app)) {
$0.$watchedApps.withLock { $0.append(app) }
}

XCTAssertEqual(env.reviewPrompts.value, 1)
}

func test_observation_toggle_off_releases_running_app_without_removing_it() async {
Expand Down Expand Up @@ -249,6 +266,7 @@ final class NightcapAppTests: XCTestCase {
let runningApps: LockIsolated<[WatchedApp]>
let acquired: LockIsolated<[String]>
let released: LockIsolated<Int>
let reviewPrompts: LockIsolated<Int>
}

private func makeEnv(
Expand All @@ -259,7 +277,8 @@ final class NightcapAppTests: XCTestCase {
running: LockIsolated(running),
runningApps: LockIsolated(runningApps),
acquired: LockIsolated([]),
released: LockIsolated(0)
released: LockIsolated(0),
reviewPrompts: LockIsolated(0)
)
}

Expand All @@ -281,6 +300,9 @@ final class NightcapAppTests: XCTestCase {
$0.powerAssertionClient.release = {
env.released.withValue { $0 += 1 }
}
$0.reviewPromptClient.requestIfAppropriate = {
env.reviewPrompts.withValue { $0 += 1 }
}
}
}
}
Loading