[#698] Add a foundation for Rating Prompt feature#699
[#698] Add a foundation for Rating Prompt feature#699thinh2k1310 wants to merge 3 commits intodevelopfrom
Conversation
📝 WalkthroughWalkthroughAdds a rating-prompt foundation: domain models and configuration, storage backed by UserDefaults, eligibility and request use cases, a StoreReviewController wrapper, presenter, and DI factories wiring these components. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App/Caller
participant RequestUC as RequestRatingPromptUseCase
participant CheckUC as ShouldShowRatingPromptUseCase
participant Storage as RatingPromptStorage
participant Presenter as DefaultRatingPromptPresenter
participant Controller as StoreReviewController
App->>RequestUC: callAsFunction(configuration)
RequestUC->>CheckUC: callAsFunction(configuration)
CheckUC->>Storage: getRatingPromptData()
Storage-->>CheckUC: RatingPromptData
CheckUC->>CheckUC: evaluate thresholds (days/launches/events)
alt Not eligible
CheckUC-->>RequestUC: false
RequestUC-->>App: false
else Eligible
CheckUC-->>RequestUC: true
RequestUC->>Presenter: show() `@MainActor`
Presenter->>Controller: requestReview() `@MainActor`
Controller->>Controller: find foreground UIWindowScene
Controller->>Controller: SKStoreReviewController.requestReview(in:)
Controller-->>Presenter: true/false
alt shown == true
RequestUC->>Storage: recordPromptShown(for: version)
opt configuration.resetCounterAfterPrompt
RequestUC->>Storage: resetCounters()
end
Storage-->>RequestUC: persisted
RequestUC-->>App: true
else
RequestUC-->>App: false
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
81e4258 to
27533b8
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift`:
- Around line 31-50: The increments in recordAppLaunch() and
recordSignificantEvent() are not atomic and can race under concurrent calls;
wrap the read-modify-write sequences (and the first-launch date set) in a single
synchronization primitive (e.g., a private serial DispatchQueue or an
NSLock/OSUnfairLock property like ratingPromptLock) so both recordAppLaunch()
and recordSignificantEvent() perform their userDefaultsManager.get... / set(...)
/ synchronize() calls inside the same lock/queue.sync block; add the lock
property to the repository, use it in both functions, and keep the existing keys
(UserDefaultsKey.ratingPromptAppLaunchCount, .ratingPromptFirstLaunchDate,
.ratingPromptSignificantEventCount) so the operations become atomic.
In `@template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift`:
- Line 7: Remove the UIKit dependency from the Data module by deleting the UIKit
import and moving the UIKit-dependent logic out of DefaultStoreReviewController;
keep the StoreReviewControllerProtocol in Data but remove any direct calls to
UIApplication.shared.connectedScenes, UIWindowScene, and
SKStoreReviewController.requestReview(in:) from
DefaultStoreReviewController—instead change DefaultStoreReviewController to
accept an injected UIWindowScene (or a simple opaque token) or replace it in the
module with a protocol-only stub, and implement the UIKit-specific adapter (that
looks up UIApplication.shared.connectedScenes, finds the active UIWindowScene,
and calls SKStoreReviewController.requestReview(in:)) in the UI/Application
layer where UIKit is allowed.
In `@template/Modules/Domain/Sources/Entities/RatingPromptConfiguration.swift`:
- Around line 9-13: The current configuration exposes minimumAppLaunches and
minimumSignificantEvents but the eligibility check in
ShouldShowRatingPromptUseCase enforces both thresholds; change the logic to
match the "or" rule or make the operator explicit: either (A) update the
eligibility check in ShouldShowRatingPromptUseCase (the function/method that
returns shouldShow) to use logical OR between app launch and significant event
checks (use || between checks that reference minimumAppLaunches and
minimumSignificantEvents), or (B) add an explicit operator to
RatingPromptConfiguration (e.g., enum ThresholdOperator { case and, or } and a
property thresholdOperator) and implement the operator-aware evaluation in
ShouldShowRatingPromptUseCase so the decision uses the configured operator when
comparing currentAppLaunches and currentSignificantEvents against
minimumAppLaunches/minimumSignificantEvents.
In `@template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift`:
- Around line 40-45: Change the flow so you only persist prompt state when the
presenter actually performed a request: update the presenter contract (e.g.,
RatingPromptPresenterProtocol) so show() returns Bool (true when a review was
actually requested), have DefaultStoreReviewController.requestReview() return
false on the no-foreground-scene path, then in RequestRatingPromptUseCase use
the Boolean result from await presenter.show() and call
storage.recordPromptShown(for: currentVersion()) and storage.resetCounters()
only when that result is true and configuration.resetCounterAfterPrompt is true.
In
`@template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift`:
- Around line 40-46: The code in ShouldShowRatingPromptUseCase incorrectly
requires both thresholds; replace the two sequential guards so eligibility
passes if either engagement signal meets its threshold: check if
data.appLaunchCount >= configuration.minimumAppLaunches OR
data.significantEventCount >= configuration.minimumSignificantEvents and return
false only when neither condition is met; update the guards/condition logic
around data and configuration to reflect this OR-based requirement.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1e9d0cc3-0ce5-45f9-8b65-06ee8ccd9e53
📒 Files selected for processing (13)
template/Modules/Data/Sources/Extensions/Container+Data.swifttemplate/Modules/Data/Sources/Managers/UserDefaultsManager/UserDefaultsKey.swifttemplate/Modules/Data/Sources/Repositories/RatingPromptStorage.swifttemplate/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swifttemplate/Modules/Domain/Sources/Entities/RatingPromptConfiguration.swifttemplate/Modules/Domain/Sources/Entities/RatingPromptData.swifttemplate/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swifttemplate/Modules/Domain/Sources/Interfaces/RatingPromptStorageProtocol.swifttemplate/Modules/Domain/Sources/UseCases/CheckForceUpdateUseCase.swifttemplate/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swifttemplate/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swifttemplate/Tuist/Interfaces/SwiftUI/Sources/Application/Dependencies/Container+Application.swifttemplate/{PROJECT_NAME}/Sources/Constants/Constants.swift
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift (2)
9-9: RedundantSendableconformance.
StoreReviewControllerProtocolalready refinesSendable, so restating it on the concrete type is noise.♻️ Proposed tweak
-final class StoreReviewController: StoreReviewControllerProtocol, Sendable { +final class StoreReviewController: StoreReviewControllerProtocol {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift` at line 9, Remove the redundant Sendable conformance from the concrete type: StoreReviewController already conforms to StoreReviewControllerProtocol which refines Sendable, so delete the extra ", Sendable" from the class declaration (StoreReviewController: StoreReviewControllerProtocol, Sendable) leaving only StoreReviewController: StoreReviewControllerProtocol to eliminate the noise.
12-21: Prefer SwiftUI-native@Environment(\.requestReview)(orAppStore.requestReview(in:)for iOS 18+) over UIKit APIs in this SwiftUI template.The current implementation uses
UIApplication.sharedandUIWindowScene(UIKit APIs), which conflicts with the template's SwiftUI-only guideline. For iOS 16+, the SwiftUIRequestReviewActionenvironment value provides a native, simpler alternative that avoids manual window scene lookup. Consider gating behind availability and migrating to the SwiftUI approach where the minimum deployment target supports it.Not blocking for this foundation PR, but worth addressing in a follow-up to ensure the template adheres to SwiftUI-only patterns.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift` around lines 12 - 21, The requestReview() method currently uses UIApplication.shared and UIWindowScene (UIKit); replace this with SwiftUI-native APIs: inject RequestReviewAction via `@Environment`(\.requestReview) and call that action for iOS 16+ (or use AppStore.requestReview(in:) when available on iOS 18+), gating with availability checks so the method falls back to returning false on unsupported OS versions; update the StoreReviewController.requestReview() implementation to use the injected requestReview environment value (or AppStore API) instead of accessing UIApplication.shared and window scenes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@template/Modules/Domain/Sources/Interfaces/StoreReviewViewControllerProtocol.swift`:
- Around line 1-9: The file name does not match the declared protocol name;
rename the file from StoreReviewViewControllerProtocol.swift to
StoreReviewControllerProtocol.swift so it matches the declared public protocol
StoreReviewControllerProtocol and follows Swift naming conventions; ensure any
imports/refs (tests, usages, docs, or comments) that referenced the old filename
are updated to the new filename so tooling/grep and module organization remain
consistent.
- Around line 5-9: Update the StoreReviewControllerProtocol to clarify the
return semantics: update the docstring for StoreReviewControllerProtocol and its
method requestReview() to state explicitly that the returned Bool indicates the
request was dispatched to the system (i.e., "request dispatched, not that the
review UI was shown"), or alternatively rename the method to
requestReviewIfPossible() and update all call sites (e.g.,
RequestRatingPromptUseCase) so their usage matches the new contract; also ensure
callers such as RequestRatingPromptUseCase only treat a true return as "request
dispatched" when deciding to call storage.recordPromptShown(for:) and
resetCounters().
---
Nitpick comments:
In
`@template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift`:
- Line 9: Remove the redundant Sendable conformance from the concrete type:
StoreReviewController already conforms to StoreReviewControllerProtocol which
refines Sendable, so delete the extra ", Sendable" from the class declaration
(StoreReviewController: StoreReviewControllerProtocol, Sendable) leaving only
StoreReviewController: StoreReviewControllerProtocol to eliminate the noise.
- Around line 12-21: The requestReview() method currently uses
UIApplication.shared and UIWindowScene (UIKit); replace this with SwiftUI-native
APIs: inject RequestReviewAction via `@Environment`(\.requestReview) and call that
action for iOS 16+ (or use AppStore.requestReview(in:) when available on iOS
18+), gating with availability checks so the method falls back to returning
false on unsupported OS versions; update the
StoreReviewController.requestReview() implementation to use the injected
requestReview environment value (or AppStore API) instead of accessing
UIApplication.shared and window scenes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2fc77bcf-1d30-4072-a175-f586f7e85ad0
📒 Files selected for processing (9)
template/Modules/Data/Sources/Extensions/Container+Data.swifttemplate/Modules/Data/Sources/Repositories/RatingPromptStorage.swifttemplate/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swifttemplate/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swifttemplate/Modules/Domain/Sources/Interfaces/StoreReviewViewControllerProtocol.swifttemplate/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swifttemplate/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swifttemplate/Tuist/Interfaces/SwiftUI/Sources/Application/Dependencies/Container+Application.swifttemplate/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift
✅ Files skipped from review due to trivial changes (1)
- template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift
🚧 Files skipped from review as they are similar to previous changes (5)
- template/Modules/Data/Sources/Extensions/Container+Data.swift
- template/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swift
- template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift
- template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift
- template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift
9f1ee8e to
c9a3fbe
Compare
What happened 👀
Introduce:
RatingPromptData: entity tracking app launch count, first launch date, last prompted version, and significant event countRatingPromptConfiguration: value type holding eligibility thresholds (min days since first launch, min launches, min significant events, reset-after-prompt flag)RatingPromptStorage: UserDefaults-backed implementation ofRatingPromptStorageProtocolDefaultRatingPromptPresenter:SKStoreReviewController-backed implementation ofRatingPromptPresenterProtocol; wraps theStoreKitcall behind an internalStoreReviewControllerProtocolto keep it testable without presenting real system UIShouldShowRatingPromptUseCase: pure eligibility logic: checks version, day count, launch count, and significant event count against the configurationRequestRatingPromptUseCase: orchestrating use case: evaluates eligibility viaShouldShowRatingPromptUseCase, triggers the presenter, and records the outcome in storageInsight 📝
N/A
Proof Of Work 📹
Will be included in Part 2
Summary by CodeRabbit