managarten/memoro/apps/mobile/docs/features/automatic-app-rating.md
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

17 KiB

Automatic App Rating Feature

Overview

The automatic app rating feature prompts users to rate the Memoro app in the App Store/Play Store after reaching specific engagement milestones. This system is designed to maximize positive ratings by requesting feedback at optimal moments—right after users have experienced success with the app.

Architecture

Components

features/rating/
├── components/
│   └── RatingPromptModal.tsx       # Modal UI component
├── hooks/
│   ├── useRating.ts                # Existing: Manual rating trigger
│   └── useRatingPrompt.ts          # New: Automatic prompt logic
├── services/
│   └── ratingService.ts            # Existing: expo-store-review wrapper
├── store/
│   └── ratingStore.ts              # Zustand store with persistence
└── index.ts                         # Public API exports

Key Features

  • Milestone-based prompts (5, 15, 50 memos)
  • Rate limiting (minimum 30 days between prompts)
  • User preferences (never ask again option)
  • Analytics tracking (all user interactions)
  • Persistent state (survives app restarts)
  • Multi-language support (i18n ready)
  • Theme-aware UI (dark/light mode)

Implementation Details

1. Rating Store (ratingStore.ts)

Purpose: Centralized state management with automatic persistence.

State:

interface RatingState {
  hasRatedApp: boolean;           // User completed rating
  hasDeclinedRating: boolean;     // User selected "Maybe Later"
  neverAskAgain: boolean;         // User selected "Don't Ask Again"
  memoCreatedCount: number;       // Total memos created
  lastPromptDate: string | null;  // Last time prompt was shown
  _hasHydrated: boolean;          // Store rehydration status
}

Actions:

  • incrementMemoCount() - Called after each memo creation
  • markRated() - User completed rating
  • markDeclined() - User deferred rating
  • markNeverAsk() - User opted out permanently
  • resetForTesting() - Development helper

Persistence: Uses Zustand's persist middleware with AsyncStorage backend. Data is automatically saved on every state change and restored on app launch.

Storage Key: memoro-rating


2. Rating Prompt Hook (useRatingPrompt.ts)

Purpose: Contains eligibility logic and modal trigger management.

Configuration Constants:

const RATING_MILESTONES = [5, 15, 50];      // Memo counts to trigger
const MIN_DAYS_BETWEEN_PROMPTS = 30;        // Cooldown period

Eligibility Criteria:

A prompt is shown when ALL conditions are met:

  1. Store has hydrated (data loaded from AsyncStorage)
  2. User hasn't rated yet (hasRatedApp === false)
  3. User hasn't opted out (neverAskAgain === false)
  4. Current memo count matches a milestone (5, 15, or 50)
  5. At least 30 days since last prompt (or first time)

API:

const {
  showPromptModal,      // Boolean: Should modal be visible?
  triggerPromptCheck,   // Function: Check eligibility & show modal
  closePrompt,          // Function: Close the modal
  isEligible,           // Boolean: Is user currently eligible?
  currentMemoCount,     // Number: Current memo count
} = useRatingPrompt();

3. Rating Prompt Modal (RatingPromptModal.tsx)

Purpose: User-facing dialog that requests the rating.

UI Structure:

┌─────────────────────────────────┐
│   Gefällt dir Memoro? (Title)   │
│                                  │
│   Du hast bereits X Memos        │
│   erstellt! 🎉 (Milestone)      │
│                                  │
│   Es würde uns sehr freuen...   │
│   (Description)                  │
│                                  │
│   [Jetzt bewerten]   (Primary)  │
│   [Vielleicht später] (Second)  │
│   [Nicht mehr fragen] (Text)    │
└─────────────────────────────────┘

Button Actions:

  1. "Jetzt bewerten" (Rate Now)

    • Calls ratingService.requestReview()
    • Opens native App Store review dialog
    • Marks as rated: markRated()
    • Tracks: rating_accepted
  2. "Vielleicht später" (Maybe Later)

    • Defers prompt for 30+ days
    • Marks as declined: markDeclined()
    • Tracks: rating_declined
  3. "Nicht mehr fragen" (Don't Ask Again)

    • Permanently suppresses prompts
    • Marks: markNeverAsk()
    • Tracks: rating_never_ask

Props:

interface RatingPromptModalProps {
  isVisible: boolean;   // Modal visibility state
  onClose: () => void;  // Close handler
}

4. Integration Points

A. Counter Increment (app/(protected)/(tabs)/index.tsx)

Location: Line 846-848

Trigger: When a new memo is detected (different from previous memo ID)

if (isNewMemo && !isFirstLoad) {
  // ... animation code ...

  // Increment rating counter for new memos
  incrementMemoCount();
}

Why here? This ensures we count user-created memos only (not initial loads or updates).

B. Global Modal Rendering (app/(protected)/_layout.tsx)

Location: Line 159

Rendered alongside other global modals:

{/* Global Rating Prompt Modal */}
<RatingPromptModal
  isVisible={showPromptModal}
  onClose={closePrompt}
/>

C. Automatic Prompt Check (app/(protected)/_layout.tsx)

Location: Lines 76-87

Trigger: When user navigates to home page

useEffect(() => {
  const isHomePage = pathname === '/' || pathname.endsWith('/index');
  if (isHomePage) {
    const timer = setTimeout(() => {
      triggerPromptCheck();
    }, 2000); // 2 second delay after landing on home page

    return () => clearTimeout(timer);
  }
}, [pathname, triggerPromptCheck]);

Why 2 seconds?

  • Gives time for memo creation to complete
  • Allows store to hydrate and update
  • Prevents immediate popup (better UX)

Analytics Events

All rating interactions are tracked in PostHog:

Event: rating_prompt_shown

{
  memo_count: number,        // Current memo count
  milestone: 5 | 15 | 50,   // Which milestone triggered
}

Event: rating_accepted

{
  memo_count: number,
  source: 'automatic_prompt', // vs 'settings' for manual
}

Event: rating_declined

{
  memo_count: number,
  action: 'maybe_later',
}

Event: rating_never_ask

{
  memo_count: number,
  action: 'never_ask',
}

Analytics Configuration: features/analytics/events.ts (Lines 66-72)


Translation Keys

German (de.json)

{
  "rating": {
    "prompt_title": "Gefällt dir Memoro?",
    "prompt_milestone": "Du hast bereits {{count}} Memos erstellt! 🎉",
    "prompt_message": "Es würde uns sehr freuen, wenn du Memoro im App Store bewerten würdest. Deine Unterstützung hilft uns, die App weiter zu verbessern.",
    "rate_now": "Jetzt bewerten",
    "maybe_later": "Vielleicht später",
    "never_ask": "Nicht mehr fragen"
  }
}

English (en.json)

{
  "rating": {
    "prompt_title": "Enjoying Memoro?",
    "prompt_milestone": "You've created {{count}} memos already! 🎉",
    "prompt_message": "We would really appreciate it if you could rate Memoro in the App Store. Your support helps us continue improving the app.",
    "rate_now": "Rate Now",
    "maybe_later": "Maybe Later",
    "never_ask": "Don't Ask Again"
  }
}

Note: The {{count}} placeholder is dynamically replaced with the actual memo count.


User Flow

Happy Path (First Time - 5 Memos)

1. User creates 5th memo
   ↓
2. Counter increments to 5
   ↓
3. User navigates to home screen
   ↓
4. After 2s delay, eligibility check runs
   ↓
5. ✅ All conditions met → Modal appears
   ↓
6. User taps "Jetzt bewerten"
   ↓
7. Native App Store review opens
   ↓
8. State: hasRatedApp = true
   ↓
9. ✅ Never shown again (unless reset)

Deferred Path (15 Memos)

1. User previously declined at 5 memos
   ↓
2. User creates 15th memo (>30 days later)
   ↓
3. Counter increments to 15
   ↓
4. User navigates to home screen
   ↓
5. After 2s delay, eligibility check runs
   ↓
6. ✅ All conditions met → Modal appears
   ↓
7. User taps "Vielleicht später"
   ↓
8. State: hasDeclinedRating = true
   ↓
9. lastPromptDate = now
   ↓
10. ⏳ Won't show again for 30+ days

Opt-Out Path

1. User sees prompt at any milestone
   ↓
2. User taps "Nicht mehr fragen"
   ↓
3. State: neverAskAgain = true
   ↓
4. ❌ Never shown again (permanent)

Configuration

Adjusting Milestones

File: features/rating/hooks/useRatingPrompt.ts (Line 8)

// Change these values to adjust when prompts appear
const RATING_MILESTONES = [5, 15, 50];

Examples:

  • More aggressive: [3, 10, 30]
  • Less aggressive: [10, 25, 100]
  • More frequent: [5, 10, 20, 50, 100]

Adjusting Cooldown Period

File: features/rating/hooks/useRatingPrompt.ts (Line 11)

// Days between prompts
const MIN_DAYS_BETWEEN_PROMPTS = 30;

Examples:

  • More frequent: 14 (2 weeks)
  • Less frequent: 60 (2 months)
  • No cooldown: 0 (not recommended)

Testing

Manual Testing

  1. Test Counter Increment:

    // In React DevTools or console
    import { useRatingStore } from '~/features/rating';
    
    console.log(useRatingStore.getState().memoCreatedCount);
    
  2. Force Prompt Display:

    // Set count to milestone
    useRatingStore.getState().incrementMemoCount(); // Repeat 5x
    
    // Navigate to home screen and wait 2s
    
  3. Reset for Testing:

    // Reset all rating state
    useRatingStore.getState().resetForTesting();
    

Testing Scenarios

Scenario Setup Expected Result
First milestone Count = 4 → 5 Modal appears after 2s on home
Already rated hasRatedApp = true No modal, ever
Opted out neverAskAgain = true No modal, ever
Too soon lastPromptDate = yesterday No modal (needs 30 days)
Between milestones Count = 12 No modal (not milestone)
Second milestone Count = 14 → 15, 30+ days Modal appears

Developer Tools

Check Current State:

import { useRatingStore } from '~/features/rating';

const state = useRatingStore.getState();
console.log({
  count: state.memoCreatedCount,
  hasRated: state.hasRatedApp,
  neverAsk: state.neverAskAgain,
  lastPrompt: state.lastPromptDate,
});

Simulate Different Counts:

// Jump to milestone
const store = useRatingStore.getState();
store.memoCreatedCount = 15;

Clear AsyncStorage:

import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.removeItem('memoro-rating');
// Then restart app to trigger fresh state

Platform-Specific Behavior

iOS

Uses SKStoreReviewController:

  • Native iOS rating dialog
  • Can only be triggered 3x per year per app version
  • User can disable in iOS Settings → App Store → In-App Ratings & Reviews
  • If disabled, nothing happens (silent failure)

Store URL Fallback:

// In ratingService.ts (Line 49)
const appStoreId = '1234567890'; // TODO: Add actual App Store ID

Android

Uses Google Play In-App Review API:

  • Native Material Design dialog
  • No rate limiting like iOS
  • More reliable display
  • Requires Google Play Services

Store URL Fallback:

// In ratingService.ts (Line 53)
const packageName = 'com.memoro.app'; // Verify this is correct

Edge Cases & Considerations

1. App Updates

  • Counter persists across app updates
  • User won't be re-prompted if already rated
  • New milestones can be added without resetting user state

2. Multiple Devices

  • State is local to each device
  • User may see prompts on multiple devices
  • This is intentional (each device = separate usage pattern)

3. Network Independence

  • Feature works completely offline
  • Only the actual rating submission requires network

4. Store Availability

  • If StoreReview.isAvailableAsync() returns false:
    • Fallback alert with link to store
    • Implemented in existing useRating.ts hook

5. Time Zone Changes

  • Date comparisons use ISO 8601 strings
  • Time zone changes won't affect cooldown logic

6. First App Launch

  • Store hydrates with default values
  • Counter starts at 0
  • First prompt at 5th memo creation

Performance Considerations

Memory

  • Store state: ~200 bytes in memory
  • AsyncStorage: ~200 bytes on disk
  • Negligible impact

CPU

  • Eligibility check: O(1) operation
  • Runs once every home page navigation
  • 2-second debounce prevents excessive checks

Network

  • Zero network calls for eligibility checks
  • Only network usage is the actual store review

Future Enhancements

Potential Improvements

  1. Sentiment Detection

    • Only prompt after successful transcriptions
    • Skip prompt if recent errors occurred
    • Use AI sentiment analysis on memo content
  2. A/B Testing

    • Test different milestone values
    • Test different message wording
    • Track conversion rates
  3. Personalization

    • Different milestones for power users
    • Custom messages based on usage patterns
    • Space-specific prompts
  4. Cross-Platform Sync

    • Sync rating state across devices
    • Avoid duplicate prompts
    • Requires backend integration
  5. Pre-Prompt Survey

    • Ask "Are you enjoying Memoro?" first
    • Only show rating if positive response
    • Capture negative feedback separately

Troubleshooting

Problem: Modal never appears

Check:

  1. Verify memo count: useRatingStore.getState().memoCreatedCount
  2. Check if user opted out: useRatingStore.getState().neverAskAgain
  3. Check if already rated: useRatingStore.getState().hasRatedApp
  4. Verify store hydrated: useRatingStore.getState()._hasHydrated
  5. Check last prompt date: useRatingStore.getState().lastPromptDate

Solution:

// Reset and test
useRatingStore.getState().resetForTesting();
// Create 5 memos and navigate to home

Problem: Modal appears too often

Check:

  • Milestone configuration might be too aggressive
  • Cooldown period might be too short

Solution:

// In useRatingPrompt.ts
const RATING_MILESTONES = [10, 25, 100];  // Less frequent
const MIN_DAYS_BETWEEN_PROMPTS = 60;       // Longer cooldown

Problem: Analytics not tracking

Check:

  1. Verify analytics enabled: Settings → Analytics
  2. Check PostHog integration
  3. Look for events in PostHog dashboard

Debug:

// Add console logging in RatingPromptModal.tsx
console.log('Rating event tracked:', eventName, properties);

Problem: Store persists between sessions

Expected Behavior: This is intentional for production.

For Development:

// Clear on every app launch (development only)
useEffect(() => {
  if (__DEV__) {
    useRatingStore.getState().resetForTesting();
  }
}, []);

Core Implementation

  • features/rating/store/ratingStore.ts - State management
  • features/rating/hooks/useRatingPrompt.ts - Logic
  • features/rating/components/RatingPromptModal.tsx - UI
  • features/rating/index.ts - Public API

Integration Points

  • app/(protected)/(tabs)/index.tsx - Counter increment
  • app/(protected)/_layout.tsx - Global modal & trigger

Configuration

  • features/analytics/events.ts - Event definitions
  • features/i18n/translations/de.json - German text
  • features/i18n/translations/en.json - English text

Existing Dependencies

  • features/rating/services/ratingService.ts - Store review wrapper
  • features/rating/hooks/useRating.ts - Manual rating trigger
  • components/atoms/BaseModal.tsx - Modal component
  • components/atoms/Button.tsx - Button component

Changelog

Version 1.0.0 (2025-01-30)

  • Initial implementation
  • Milestone-based prompts (5, 15, 50 memos)
  • 30-day cooldown period
  • Persistent state with AsyncStorage
  • Analytics tracking
  • Multi-language support (DE, EN)
  • Theme-aware UI

Maintenance

Regular Checks

  • Monitor analytics conversion rates monthly
  • Review milestone effectiveness quarterly
  • Update translations as needed
  • Test on new iOS/Android versions

When to Update

  1. App Store ID changes (iOS)
    • Update in ratingService.ts line 49
  2. Package name changes (Android)
    • Update in ratingService.ts line 53
  3. New languages added
    • Add translations in features/i18n/translations/
  4. Milestone strategy changes
    • Update in useRatingPrompt.ts line 8

Contact

For questions or issues related to this feature:

  • Check existing issues in GitHub
  • Review analytics in PostHog dashboard
  • Contact development team

Last Updated: 2025-01-30 Author: Claude Code Version: 1.0.0