
Review Prompt
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates smart App Store review prompt infrastructure with configurable trigger conditions, platform detection, and timing logic.
About
Generates StoreKit-based App Store review prompting with configurable trigger conditions, platform detection, and proper timing logic. A developer uses it to request ratings and reviews at the right moment in an app.
- Configurable trigger conditions and timing logic
- StoreKit review request with platform detection
Review Prompt by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill review-promptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates smart App Store review prompt infrastructure with configurable trigger conditions, platform detection, and timing logic.
Files
Review Prompt Generator
Generate smart App Store review prompting with configurable trigger conditions, platform detection, and proper timing logic.
When This Skill Activates
Use this skill when the user:
- Asks to "add review prompt" or "request reviews"
- Mentions "App Store rating" or "app reviews"
- Wants to "prompt for ratings" or "ask for reviews"
- Asks about "StoreKit review" or "SKStoreReviewController"
Platform Detection (CRITICAL)
This skill only applies to App Store distributed apps.
iOS Apps
- Always applicable (iOS apps require App Store)
macOS Apps
Detection steps: 1. Check for com.apple.application-identifier entitlement 2. Look for Mac App Store related code 3. If unclear, ASK THE USER:
- "Is this app distributed via Mac App Store or direct download?"
If NOT App Store:
- Explain that StoreKit reviews only work for App Store apps
- Offer alternative: In-app feedback form
- Skip generation or generate feedback form instead
Pre-Generation Checks
1. Project Context Detection
- [ ] Determine platform (iOS/macOS)
- [ ] Check distribution method (App Store vs direct)
- [ ] Search for existing review prompt code
- [ ] Identify App entry point
2. Conflict Detection
Search for existing implementations:
Grep: "requestReview" or "SKStoreReviewController" or "StoreKit"
Glob: **/*Review*.swiftIf found, ask user:
- Replace existing implementation?
- Enhance with better timing logic?
Configuration Questions
Ask user via AskUserQuestion:
1. Trigger conditions? (multi-select)
- Session count (e.g., after 5 sessions)
- Days since install (e.g., after 3 days)
- Positive actions (e.g., after completing a task)
- Feature usage (e.g., after using key feature 3 times)
2. Minimum thresholds?
- Sessions before first prompt: 3-5 (default: 3)
- Days before first prompt: 2-7 (default: 3)
3. Cool-down period?
- Days between prompts: 30-90 (default: 60)
- Apple limits to 3 prompts/year anyway
4. Debug mode?
- Include debug override for testing?
Generation Process
Step 1: Create Core Files
Generate these files: 1. ReviewPromptManager.swift - Core logic and timing 2. ReviewPromptCondition.swift - Configurable conditions 3. ReviewPromptStorage.swift - Persistence for tracking
Step 2: Determine File Location
Check project structure:
- If
Sources/exists →Sources/Reviews/ - If
App/exists →App/Reviews/ - Otherwise →
Reviews/
Step 3: Add Platform Guards
For macOS, include:
#if os(macOS)
// Check if running from App Store
guard Bundle.main.appStoreReceiptURL?.lastPathComponent != "sandboxReceipt" else {
// Running in sandbox but not App Store - skip
return
}
#endifOutput Format
After generation, provide:
Files Created
Sources/Reviews/
├── ReviewPromptManager.swift # Core logic
├── ReviewPromptCondition.swift # Conditions enum
└── ReviewPromptStorage.swift # UserDefaults persistenceIntegration Steps
Option 1: Automatic (Recommended)
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
ReviewPromptManager.shared.incrementSession()
}
}
}
}
// In relevant places (after positive actions):
ReviewPromptManager.shared.recordPositiveAction()
ReviewPromptManager.shared.requestReviewIfAppropriate()Option 2: Manual Trigger Points
// After user completes a significant action
func completeTask() {
// ... task completion logic ...
ReviewPromptManager.shared.recordPositiveAction()
ReviewPromptManager.shared.requestReviewIfAppropriate()
}Apple's Guidelines
- System limits to 3 prompts per 365-day period
- Prompt appears at system's discretion (not guaranteed)
- Never prompt after negative experience
- Don't prompt on first launch
- Don't interrupt user's workflow
Testing Instructions
1. Debug Mode: Set ReviewPromptManager.debugAlwaysShow = true 2. Reset State: Call ReviewPromptStorage.reset() 3. Simulate: Increment sessions/actions in debug builds 4. Note: Actual prompt may not show in Simulator
App Store Review URL (Alternative)
For custom UI or macOS direct distribution:
// Deep link to App Store review page
let appID = "YOUR_APP_ID"
let url = URL(string: "https://apps.apple.com/app/id\(appID)?action=write-review")!
NSWorkspace.shared.open(url) // macOS
UIApplication.shared.open(url) // iOSReferences
- storekit-patterns.md - Best practices and timing strategies
- templates/ - All template files
StoreKit Review Patterns and Best Practices
Apple's Review Prompt Rules
System Limitations
- Maximum 3 prompts per 365-day period per app
- System decides whether to actually show the prompt
- No feedback to app about whether prompt was shown
- Prompt may not show in Simulator
Guidelines
1. Don't prompt immediately - Wait for user to use app 2. Don't interrupt - Choose natural break points 3. After positive moments - User just accomplished something 4. Never after negative - Errors, crashes, frustration 5. Don't ask repeatedly - Respect the 3/year limit
Timing Strategies
Good Timing
- After completing a task successfully
- After achieving a milestone (10th entry, etc.)
- After using the app for several sessions
- At natural pause points (returning from background)
Bad Timing
- On first launch
- During active workflow
- After an error
- When user is trying to do something
- Right after a purchase
Condition-Based Triggering
Session Count
// Don't ask until user has come back multiple times
func shouldPromptBasedOnSessions() -> Bool {
sessionCount >= minimumSessions
}Why: Users who return are more likely to be satisfied
Days Since Install
// Give users time to evaluate the app
func shouldPromptBasedOnDays() -> Bool {
daysSinceInstall >= minimumDays
}Why: First impressions may not reflect long-term value
Positive Actions
// Track successful completions
func shouldPromptBasedOnActions() -> Bool {
positiveActionCount >= minimumPositiveActions
}Why: Users who accomplish things are happier
Combined Conditions
// Require ALL conditions
func shouldPrompt() -> Bool {
sessionCount >= minimumSessions &&
daysSinceInstall >= minimumDays &&
positiveActionCount >= minimumPositiveActions &&
daysSinceLastPrompt >= cooldownDays
}Cool-Down Strategy
Why Cool-Down Matters
- Apple limits to 3/year anyway
- Repeated prompts annoy users
- Space out for maximum effectiveness
Recommended Cool-Down
// 60-90 days between prompts
let cooldownDays = 60
func canPromptAgain() -> Bool {
guard let lastPromptDate = lastPromptDate else { return true }
let daysSince = Calendar.current.dateComponents([.day], from: lastPromptDate, to: Date()).day ?? 0
return daysSince >= cooldownDays
}Platform-Specific Considerations
iOS
import StoreKit
func requestReview() {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
}macOS (App Store Only)
import StoreKit
func requestReview() {
// Check if running from App Store
guard isAppStoreVersion() else {
// Direct distribution - use alternative
openAppStoreReviewPage()
return
}
SKStoreReviewController.requestReview()
}
func isAppStoreVersion() -> Bool {
// sandboxReceipt indicates development/TestFlight
// Receipt present without sandbox indicates App Store
guard let receiptURL = Bundle.main.appStoreReceiptURL else { return false }
return FileManager.default.fileExists(atPath: receiptURL.path) &&
!receiptURL.path.contains("sandboxReceipt")
}macOS (Direct Distribution Alternative)
func openAppStoreReviewPage() {
let appID = "YOUR_APP_ID"
if let url = URL(string: "https://apps.apple.com/app/id\(appID)?action=write-review") {
NSWorkspace.shared.open(url)
}
}Debug and Testing
Debug Override
#if DEBUG
static var debugAlwaysShow = false
func requestReviewIfAppropriate() {
if Self.debugAlwaysShow {
requestReview()
return
}
// Normal logic...
}
#endifState Reset
static func resetForTesting() {
UserDefaults.standard.removeObject(forKey: Keys.sessionCount)
UserDefaults.standard.removeObject(forKey: Keys.installDate)
UserDefaults.standard.removeObject(forKey: Keys.lastPromptDate)
UserDefaults.standard.removeObject(forKey: Keys.positiveActionCount)
}Simulator Notes
- Prompt may not appear in Simulator
- Test on physical device for actual behavior
- Use debug logging to verify conditions are met
Tracking What Worked
Analytics Integration
func requestReviewIfAppropriate() {
guard shouldPrompt() else { return }
// Track that we attempted to show prompt
analytics.track(.reviewPromptAttempted(
sessions: sessionCount,
days: daysSinceInstall,
actions: positiveActionCount
))
requestReview()
recordPromptDate()
}A/B Testing Conditions
// Test different thresholds
let thresholds: ReviewThresholds = FeatureFlags.reviewPromptVariant == .aggressive
? .init(sessions: 2, days: 1, actions: 3)
: .init(sessions: 5, days: 7, actions: 5)In-App Feedback Alternative
For when App Store review isn't appropriate:
struct FeedbackView: View {
@State private var rating: Int = 0
@State private var feedback: String = ""
var body: some View {
VStack {
Text("How are we doing?")
// Star rating
HStack {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.onTapGesture { rating = star }
}
}
if rating > 0 {
if rating >= 4 {
// Happy user - ask for App Store review
Button("Rate on App Store") {
ReviewPromptManager.shared.requestReview()
}
} else {
// Unhappy user - capture feedback internally
TextField("How can we improve?", text: $feedback)
Button("Send Feedback") {
sendFeedback(rating: rating, feedback: feedback)
}
}
}
}
}
}Common Anti-Patterns
Don't Do This
// Bad: Prompting on every launch
func applicationDidBecomeActive() {
SKStoreReviewController.requestReview() // NO!
}
// Bad: Prompting after errors
func handleError(_ error: Error) {
showErrorAlert(error)
requestReview() // NO! User is frustrated
}
// Bad: Prompting immediately after purchase
func handlePurchaseSuccess() {
// User just paid - let them use what they bought
requestReview() // NO! Too early
}
// Bad: Begging
func showCustomPrompt() {
"Please rate us 5 stars!" // NO! Let the system prompt
}Do This Instead
// Good: After positive action with conditions met
func taskCompleted() {
saveTask()
celebrateCompletion()
// Check all conditions before prompting
if shouldPromptForReview() {
// Small delay to not interrupt celebration
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
requestReviewIfAppropriate()
}
}
}import Foundation
/// Conditions that must be met before showing a review prompt.
///
/// Configure these thresholds based on your app's usage patterns.
/// More engaged users are more likely to leave positive reviews.
struct ReviewPromptCondition {
/// Minimum number of app sessions before prompting.
/// Default: 3 (user has returned multiple times)
let minimumSessions: Int
/// Minimum days since first launch before prompting.
/// Default: 3 (user has had time to evaluate)
let minimumDays: Int
/// Minimum positive actions before prompting.
/// Default: 5 (user has accomplished things)
let minimumPositiveActions: Int
/// Minimum days between review prompts.
/// Default: 60 (Apple limits to 3/year anyway)
let cooldownDays: Int
/// Default conditions suitable for most apps.
static let `default` = ReviewPromptCondition(
minimumSessions: 3,
minimumDays: 3,
minimumPositiveActions: 5,
cooldownDays: 60
)
/// More conservative conditions for apps with longer engagement cycles.
static let conservative = ReviewPromptCondition(
minimumSessions: 5,
minimumDays: 7,
minimumPositiveActions: 10,
cooldownDays: 90
)
/// More aggressive conditions for apps with quick value delivery.
/// Use with caution - may annoy users if value isn't immediately clear.
static let eager = ReviewPromptCondition(
minimumSessions: 2,
minimumDays: 1,
minimumPositiveActions: 3,
cooldownDays: 45
)
}
// MARK: - Condition Checking
extension ReviewPromptCondition {
/// Check if all conditions are met.
func isSatisfied(
sessions: Int,
daysSinceInstall: Int,
positiveActions: Int,
daysSinceLastPrompt: Int?
) -> Bool {
// Check minimum thresholds
guard sessions >= minimumSessions else { return false }
guard daysSinceInstall >= minimumDays else { return false }
guard positiveActions >= minimumPositiveActions else { return false }
// Check cooldown
if let daysSinceLastPrompt = daysSinceLastPrompt {
guard daysSinceLastPrompt >= cooldownDays else { return false }
}
return true
}
}
import StoreKit
#if canImport(UIKit)
import UIKit
#endif
#if canImport(AppKit)
import AppKit
#endif
/// Manages App Store review prompts with smart timing.
///
/// Usage:
/// ```swift
/// // On app launch/foreground
/// ReviewPromptManager.shared.incrementSession()
///
/// // After positive actions (task completion, etc.)
/// ReviewPromptManager.shared.recordPositiveAction()
/// ReviewPromptManager.shared.requestReviewIfAppropriate()
/// ```
///
/// Configure conditions by setting `ReviewPromptManager.shared.conditions`.
final class ReviewPromptManager {
// MARK: - Singleton
static let shared = ReviewPromptManager()
private init() {}
// MARK: - Configuration
/// Conditions that must be met before showing a prompt.
var conditions = ReviewPromptCondition.default
#if DEBUG
/// Set to `true` to always show prompt (debug only).
var debugAlwaysShow = false
#endif
// MARK: - Session Tracking
/// Increment session count. Call on app launch or foreground.
func incrementSession() {
ReviewPromptStorage.incrementSession()
// Initialize install date on first session
_ = ReviewPromptStorage.installDate
}
// MARK: - Action Tracking
/// Record a positive user action.
/// Call when user completes a task, achieves a goal, etc.
func recordPositiveAction() {
ReviewPromptStorage.recordPositiveAction()
}
// MARK: - Review Request
/// Request a review if all conditions are met.
///
/// Call this after positive actions, not on every interaction.
/// The system decides whether to actually show the prompt.
func requestReviewIfAppropriate() {
#if DEBUG
if debugAlwaysShow {
requestReview()
return
}
#endif
guard shouldRequestReview() else { return }
requestReview()
ReviewPromptStorage.recordPromptShown()
}
/// Check if conditions are met for requesting a review.
func shouldRequestReview() -> Bool {
// Check platform eligibility first
guard isAppStoreVersion() else { return false }
return conditions.isSatisfied(
sessions: ReviewPromptStorage.sessionCount,
daysSinceInstall: ReviewPromptStorage.daysSinceInstall,
positiveActions: ReviewPromptStorage.positiveActionCount,
daysSinceLastPrompt: ReviewPromptStorage.daysSinceLastPrompt
)
}
// MARK: - Platform Detection
/// Check if app is running from App Store (not dev/TestFlight).
private func isAppStoreVersion() -> Bool {
#if DEBUG
// Always allow in debug for testing
return true
#else
// Check for App Store receipt
guard let receiptURL = Bundle.main.appStoreReceiptURL else { return false }
let receiptExists = FileManager.default.fileExists(atPath: receiptURL.path)
#if os(macOS)
// On macOS, sandboxReceipt indicates development/TestFlight
if receiptURL.lastPathComponent == "sandboxReceipt" {
return false
}
#endif
return receiptExists
#endif
}
// MARK: - Review Request Implementation
private func requestReview() {
#if os(iOS)
requestReviewiOS()
#elseif os(macOS)
requestReviewmacOS()
#endif
}
#if os(iOS)
private func requestReviewiOS() {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
}
#endif
#if os(macOS)
private func requestReviewmacOS() {
SKStoreReviewController.requestReview()
}
#endif
// MARK: - Alternative: Direct App Store Link
/// Open App Store page for writing a review.
/// Use for macOS direct distribution or custom UI.
///
/// - Parameter appID: Your App Store app ID
func openAppStoreReviewPage(appID: String) {
guard let url = URL(string: "https://apps.apple.com/app/id\(appID)?action=write-review") else {
return
}
#if os(iOS)
UIApplication.shared.open(url)
#elseif os(macOS)
NSWorkspace.shared.open(url)
#endif
}
// MARK: - Debug/Testing
/// Reset all tracking data. Use for testing.
func reset() {
ReviewPromptStorage.reset()
}
/// Get current tracking stats for debugging.
var debugStats: String {
"""
Sessions: \(ReviewPromptStorage.sessionCount)
Days since install: \(ReviewPromptStorage.daysSinceInstall)
Positive actions: \(ReviewPromptStorage.positiveActionCount)
Days since last prompt: \(ReviewPromptStorage.daysSinceLastPrompt.map(String.init) ?? "never")
Should prompt: \(shouldRequestReview())
"""
}
}
// MARK: - SwiftUI Integration
import SwiftUI
extension View {
/// Track session on view appear and request review if appropriate.
///
/// Usage:
/// ```swift
/// ContentView()
/// .trackSessionAndRequestReview()
/// ```
func trackSessionAndRequestReview() -> some View {
onAppear {
ReviewPromptManager.shared.incrementSession()
}
}
/// Request review after a delay if conditions are met.
/// Use after positive actions.
///
/// - Parameter delay: Seconds to wait before requesting (default: 1.0)
func requestReviewIfAppropriate(delay: TimeInterval = 1.0) -> some View {
onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
ReviewPromptManager.shared.requestReviewIfAppropriate()
}
}
}
}
import Foundation
/// Persistent storage for review prompt tracking.
///
/// Tracks session count, install date, positive actions, and last prompt date
/// to determine when to show review prompts.
enum ReviewPromptStorage {
// MARK: - Keys
private enum Keys {
static let sessionCount = "reviewPrompt.sessionCount"
static let installDate = "reviewPrompt.installDate"
static let positiveActionCount = "reviewPrompt.positiveActionCount"
static let lastPromptDate = "reviewPrompt.lastPromptDate"
}
// MARK: - Session Count
/// Number of app sessions (launches/foreground events).
static var sessionCount: Int {
get { UserDefaults.standard.integer(forKey: Keys.sessionCount) }
set { UserDefaults.standard.set(newValue, forKey: Keys.sessionCount) }
}
/// Increment session count. Call on app launch or foreground.
static func incrementSession() {
sessionCount += 1
}
// MARK: - Install Date
/// Date of first app launch.
static var installDate: Date {
get {
if let date = UserDefaults.standard.object(forKey: Keys.installDate) as? Date {
return date
}
// First access - set to now
let now = Date()
UserDefaults.standard.set(now, forKey: Keys.installDate)
return now
}
}
/// Days since first launch.
static var daysSinceInstall: Int {
Calendar.current.dateComponents([.day], from: installDate, to: Date()).day ?? 0
}
// MARK: - Positive Actions
/// Count of positive user actions (task completions, etc.).
static var positiveActionCount: Int {
get { UserDefaults.standard.integer(forKey: Keys.positiveActionCount) }
set { UserDefaults.standard.set(newValue, forKey: Keys.positiveActionCount) }
}
/// Record a positive action. Call when user accomplishes something.
static func recordPositiveAction() {
positiveActionCount += 1
}
// MARK: - Last Prompt Date
/// Date of last review prompt attempt.
static var lastPromptDate: Date? {
get { UserDefaults.standard.object(forKey: Keys.lastPromptDate) as? Date }
set { UserDefaults.standard.set(newValue, forKey: Keys.lastPromptDate) }
}
/// Days since last prompt (nil if never prompted).
static var daysSinceLastPrompt: Int? {
guard let lastDate = lastPromptDate else { return nil }
return Calendar.current.dateComponents([.day], from: lastDate, to: Date()).day
}
/// Record that a prompt was shown.
static func recordPromptShown() {
lastPromptDate = Date()
}
// MARK: - Reset (Testing)
/// Reset all tracking data. Use for testing.
static func reset() {
UserDefaults.standard.removeObject(forKey: Keys.sessionCount)
UserDefaults.standard.removeObject(forKey: Keys.installDate)
UserDefaults.standard.removeObject(forKey: Keys.positiveActionCount)
UserDefaults.standard.removeObject(forKey: Keys.lastPromptDate)
}
}