Skip to content

[#698] Add a foundation for Rating Prompt feature#699

Open
thinh2k1310 wants to merge 3 commits intodevelopfrom
feature/#698-foundation-for-rating-prompt
Open

[#698] Add a foundation for Rating Prompt feature#699
thinh2k1310 wants to merge 3 commits intodevelopfrom
feature/#698-foundation-for-rating-prompt

Conversation

@thinh2k1310
Copy link
Copy Markdown
Contributor

@thinh2k1310 thinh2k1310 commented Apr 20, 2026

What happened 👀

Introduce:

  • RatingPromptData: entity tracking app launch count, first launch date, last prompted version, and significant event count
  • RatingPromptConfiguration: value type holding eligibility thresholds (min days since first launch, min launches, min significant events, reset-after-prompt flag)
  • RatingPromptStorage: UserDefaults-backed implementation of RatingPromptStorageProtocol
  • DefaultRatingPromptPresenter: SKStoreReviewController-backed implementation of RatingPromptPresenterProtocol; wraps the StoreKit call behind an internal StoreReviewControllerProtocol to keep it testable without presenting real system UI
  • ShouldShowRatingPromptUseCase: pure eligibility logic: checks version, day count, launch count, and significant event count against the configuration
  • RequestRatingPromptUseCase: orchestrating use case: evaluates eligibility via ShouldShowRatingPromptUseCase, triggers the presenter, and records the outcome in storage
  • Todo:
  • Integration
  • Unit tests

Insight 📝

N/A

Proof Of Work 📹

Will be included in Part 2

Summary by CodeRabbit

  • New Features
    • Added a complete in-app rating prompt system with configurable trigger conditions (min days, launches, significant events) and optional counter reset after prompting
    • Persistent tracking of launches, first-launch date, significant events, and per-version prompt history to avoid duplicate prompts
    • Async main-thread review request flow with safe scene detection and a presenter abstraction
    • Dependency-injection support for storage, presenter, and use cases

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Domain Models & Configuration
template/Modules/Domain/Sources/Entities/RatingPromptConfiguration.swift, template/Modules/Domain/Sources/Entities/RatingPromptData.swift
New public data types for rating-prompt state and configuration, with computed helpers (daysSinceFirstLaunch, hasBeenPromptedForCurrentVersion).
Domain Protocols
template/Modules/Domain/Sources/Interfaces/RatingPromptStorageProtocol.swift, template/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swift, template/Modules/Domain/Sources/Interfaces/StoreReviewViewControllerProtocol.swift
New protocols defining storage, presenter, and store-review controller contracts with main-actor and Sendable constraints.
Domain Use Cases
template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift, template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift
Added use cases: eligibility check against configuration and orchestrator that requests presentation then records/reset counters on success.
Domain Update
template/Modules/Domain/Sources/UseCases/CheckForceUpdateUseCase.swift
Promoted defaultCurrentVersion() visibility to public static.
Data Storage & UserDefaults
template/Modules/Data/Sources/Managers/UserDefaultsManager/UserDefaultsKey.swift, template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift
Added four UserDefaults keys and a new RatingPromptStorage implementation handling get/record/reset/clear operations with synchronization and locking for concurrent updates.
Presenter & Store Review
template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift, template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift
Added DefaultRatingPromptPresenter delegating to a StoreReviewController wrapper that finds the foreground UIWindowScene and calls SKStoreReviewController.requestReview(in:).
Dependency Injection
template/Modules/Data/Sources/Extensions/Container+Data.swift, template/Tuist/Interfaces/SwiftUI/Sources/Application/Dependencies/Container+Application.swift
Registered DI factories: ratingPromptStorage, ratingPromptPresenter, shouldShowRatingPromptUseCase, and requestRatingPromptUseCase.
Minor
template/{PROJECT_NAME}/Sources/Constants/Constants.swift
Added import Foundation for URL usage.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • suho
  • patcharapon-j
  • minhnimble
  • nmint8m
  • vnntsu
  • Shayokh144
  • nkhanh44
  • dquangit
  • roman-nimble
  • kevinhoangpq
  • phongvhd93
  • markgravity

Poem

🐰 A little hop, a gentle nudge,
Counters tally each app judge,
When days and launches line up right,
A review prompt hops into sight,
Hop on, leave a star tonight! ⭐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main change: introducing foundational components for a Rating Prompt feature referenced by issue #698.
Linked Issues check ✅ Passed All acceptance criteria from #698 are met: storage layer with UserDefaults tracking, eligibility logic via use cases, and SKStoreReviewController wrapper for testability.
Out of Scope Changes check ✅ Passed All changes directly support the Rating Prompt feature foundation; minor import addition and public method visibility change are supporting modifications with no out-of-scope additions.
Description check ✅ Passed The PR description covers all required sections from the template with appropriate detail for a feature introducing rating prompt foundation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#698-foundation-for-rating-prompt

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between da5d9e5 and 27533b8.

📒 Files selected for processing (13)
  • template/Modules/Data/Sources/Extensions/Container+Data.swift
  • template/Modules/Data/Sources/Managers/UserDefaultsManager/UserDefaultsKey.swift
  • template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift
  • template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift
  • template/Modules/Domain/Sources/Entities/RatingPromptConfiguration.swift
  • template/Modules/Domain/Sources/Entities/RatingPromptData.swift
  • template/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swift
  • template/Modules/Domain/Sources/Interfaces/RatingPromptStorageProtocol.swift
  • template/Modules/Domain/Sources/UseCases/CheckForceUpdateUseCase.swift
  • template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift
  • template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift
  • template/Tuist/Interfaces/SwiftUI/Sources/Application/Dependencies/Container+Application.swift
  • template/{PROJECT_NAME}/Sources/Constants/Constants.swift

Comment thread template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift Outdated
Comment thread template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift Outdated
Comment thread template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift Outdated
Comment thread template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
template/Tuist/Interfaces/SwiftUI/Sources/Application/Services/StoreReviewController.swift (2)

9-9: Redundant Sendable conformance.

StoreReviewControllerProtocol already refines Sendable, 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) (or AppStore.requestReview(in:) for iOS 18+) over UIKit APIs in this SwiftUI template.

The current implementation uses UIApplication.shared and UIWindowScene (UIKit APIs), which conflicts with the template's SwiftUI-only guideline. For iOS 16+, the SwiftUI RequestReviewAction environment 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

📥 Commits

Reviewing files that changed from the base of the PR and between 27533b8 and 9f1ee8e.

📒 Files selected for processing (9)
  • template/Modules/Data/Sources/Extensions/Container+Data.swift
  • template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift
  • template/Modules/Data/Sources/Services/DefaultRatingPromptPresenter.swift
  • template/Modules/Domain/Sources/Interfaces/RatingPromptPresenterProtocol.swift
  • template/Modules/Domain/Sources/Interfaces/StoreReviewViewControllerProtocol.swift
  • template/Modules/Domain/Sources/UseCases/RequestRatingPromptUseCase.swift
  • template/Modules/Domain/Sources/UseCases/ShouldShowRatingPromptUseCase.swift
  • template/Tuist/Interfaces/SwiftUI/Sources/Application/Dependencies/Container+Application.swift
  • template/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

@thinh2k1310 thinh2k1310 force-pushed the feature/#698-foundation-for-rating-prompt branch from 9f1ee8e to c9a3fbe Compare April 21, 2026 15:40
Comment thread template/Modules/Data/Sources/Repositories/RatingPromptStorage.swift Outdated
Comment thread template/{PROJECT_NAME}/Sources/Constants/Constants.swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a foundation for Rating Prompt feature

2 participants