# Rating prompt shown at a natural moment, not on first launch or during a task

- **Pattern:** `ab-000512` (`app-store-review-blockers.policy-compliance.rate-prompt-timing`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000512
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000512)

## Why it matters

A rating prompt shown on first launch — before the user has used the app — or during a high-stakes task like checkout produces dismissals and one-star reviews from irritated users. Apple's guideline 1.1.7 discourages disruptive or poorly-timed prompts, and a pattern of low ratings from premature prompts signals the platform that your app has quality issues. More practically: users who dismiss an early prompt are less likely to rate the app later. A timer-based prompt (`setTimeout(requestReview, 30000)` from app start) is a common form of this failure that appears in code review as a clear sign of a copy-paste implementation.

## Severity rationale

Low because poor timing causes rating quality degradation rather than direct rejection, though repeated user complaints about intrusive prompts can attract guideline scrutiny.

## Remediation

Tie the rating prompt to a specific, meaningful success event — never to a timer or to app launch.

```tsx
// src/hooks/useRatingPrompt.ts
import * as StoreReview from 'expo-store-review';
import AsyncStorage from '@react-native-async-storage/async-storage';

export async function maybePromptRating(trigger: 'task_complete' | 'milestone' | 'streak') {
  const sessions = parseInt(await AsyncStorage.getItem('session_count') ?? '0');
  const hasRated = await AsyncStorage.getItem('has_rated_v1');

  // Only prompt after 5+ sessions, never more than once per version
  if (sessions < 5 || hasRated) return;

  if (await StoreReview.hasAction()) {
    await StoreReview.requestReview();
    await AsyncStorage.setItem('has_rated_v1', 'true');
  }
}
```

Call `maybePromptRating` from a positive completion handler — after a task finishes, after a streak milestone, after a successful purchase. Remove any `setTimeout`-based prompt trigger from `App.tsx` or root `useEffect`.

## Detection

- **ID:** `rate-prompt-timing`
- **Severity:** `low`
- **What to look for:** Count all relevant instances and enumerate each. Search for `StoreReview.requestReview()`, `SKStoreReviewController.requestReview()`, `InAppReview.Manager.requestReview()`, `InAppReviewInfo.create()` (Flutter). Find the call sites and determine when they are triggered. Red flags: called in `useEffect` on the root component (first launch); called with a timer (e.g., `setTimeout(requestReview, 30000)` from app launch); called during active task completion where interruption is disruptive (during form submission, during checkout, during a game match). Green flags: called after a key success moment (task completed, milestone reached, positive interaction).
- **Pass criteria:** Rating prompt is triggered at a natural, positive moment in the user journey — after completing a meaningful action, reaching a milestone, or after enough usage to form an opinion. At least 1 implementation must be verified. Not on first launch, not mid-task.
- **Fail criteria:** Rating prompt triggered on first launch; triggered with a hardcoded timer from app start; triggered during checkout, form submission, or any active task; triggered before the user has meaningfully used the app.
- **Skip (N/A) when:** App does not use any rating prompt (no `StoreReview` library imported or called).
- **Detail on fail:** `"StoreReview.requestReview() called in useEffect on App.tsx root — shows on first launch before any user interaction"` or `"Rating prompt triggered 30 seconds after app start with no user action"`
- **Remediation:** Rating prompts shown at bad moments train users to dismiss them and hurt your ratings.
  1. Track a meaningful trigger event before showing the prompt:
     ```tsx
     // After user completes a significant action
     async function onTaskCompleted() {
       setCompletedCount(prev => prev + 1);
       if (completedCount >= 5 && !hasRated) {
         await StoreReview.requestReview();
         setHasRated(true);
       }
     }
     ```
  2. Good trigger moments: after completing 3+ tasks, after a streak milestone, after a positive session
  3. Bad trigger moments: app launch, during payment, during onboarding, during active gameplay
  4. Apple limits the system dialog to 3 times per year automatically — your code logic should also respect this

## External references

- external apple-guideline-1.1.7-rating-prompt-timing — https://developer.apple.com/app-store/review/guidelines/#objectionable-content

Taxons: regulatory-conformance, user-experience

HTML version: https://auditbuffet.com/patterns/ab-000512
