mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 09:19:39 +02:00
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>
689 lines
17 KiB
Markdown
689 lines
17 KiB
Markdown
# 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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
{/* 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
|
|
|
|
```typescript
|
|
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`
|
|
```typescript
|
|
{
|
|
memo_count: number, // Current memo count
|
|
milestone: 5 | 15 | 50, // Which milestone triggered
|
|
}
|
|
```
|
|
|
|
### Event: `rating_accepted`
|
|
```typescript
|
|
{
|
|
memo_count: number,
|
|
source: 'automatic_prompt', // vs 'settings' for manual
|
|
}
|
|
```
|
|
|
|
### Event: `rating_declined`
|
|
```typescript
|
|
{
|
|
memo_count: number,
|
|
action: 'maybe_later',
|
|
}
|
|
```
|
|
|
|
### Event: `rating_never_ask`
|
|
```typescript
|
|
{
|
|
memo_count: number,
|
|
action: 'never_ask',
|
|
}
|
|
```
|
|
|
|
**Analytics Configuration:** `features/analytics/events.ts` (Lines 66-72)
|
|
|
|
---
|
|
|
|
## Translation Keys
|
|
|
|
### German (`de.json`)
|
|
|
|
```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`)
|
|
|
|
```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)
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// In React DevTools or console
|
|
import { useRatingStore } from '~/features/rating';
|
|
|
|
console.log(useRatingStore.getState().memoCreatedCount);
|
|
```
|
|
|
|
2. **Force Prompt Display:**
|
|
```typescript
|
|
// Set count to milestone
|
|
useRatingStore.getState().incrementMemoCount(); // Repeat 5x
|
|
|
|
// Navigate to home screen and wait 2s
|
|
```
|
|
|
|
3. **Reset for Testing:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// Jump to milestone
|
|
const store = useRatingStore.getState();
|
|
store.memoCreatedCount = 15;
|
|
```
|
|
|
|
**Clear AsyncStorage:**
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// Clear on every app launch (development only)
|
|
useEffect(() => {
|
|
if (__DEV__) {
|
|
useRatingStore.getState().resetForTesting();
|
|
}
|
|
}, []);
|
|
```
|
|
|
|
---
|
|
|
|
## Related Files
|
|
|
|
### 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
|