native-update
Version:
Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.
769 lines (623 loc) • 19.8 kB
Markdown
# App Review Implementation Guide
This comprehensive guide explains how to implement in-app review functionality in your Capacitor application using the NativeUpdate plugin.
## Table of Contents
- [Overview](#overview)
- [Why In-App Reviews Matter](#why-in-app-reviews-matter)
- [Platform Guidelines](#platform-guidelines)
- [Setup Guide](#setup-guide)
- [Implementation Steps](#implementation-steps)
- [Best Practices](#best-practices)
- [Analytics Integration](#analytics-integration)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
## Overview
The App Review feature allows users to rate and review your app without leaving it, significantly improving the likelihood of receiving feedback.
### Key Benefits
- 📈 **Higher Review Rates**: 2-3x more reviews than traditional methods
- 🎯 **Better Timing**: Ask for reviews at optimal moments
- 😊 **Improved UX**: No app switching required
- 📊 **Better Ratings**: Happy users are more likely to rate
### Platform Support
- **Android**: Google Play In-App Review API (Android 5.0+)
- **iOS**: SKStoreReviewController (iOS 10.3+)
- **Web**: Fallback to custom UI or redirect
## Why In-App Reviews Matter
### Statistics
- Apps with 4+ star ratings see **2x more downloads**
- **70% of users** look at ratings before downloading
- In-app review prompts have **4-5x higher engagement** than external links
### Impact on ASO (App Store Optimization)
- Higher ratings improve search ranking
- More reviews increase credibility
- Recent reviews show active development
## Platform Guidelines
### Apple App Store Guidelines
1. **Frequency Limits**:
- Maximum 3 prompts per 365 days
- System may show prompt less frequently
- Cannot check if review was submitted
2. **Requirements**:
- Cannot incentivize reviews
- Cannot gate features behind reviews
- Must use official API (no custom UI)
### Google Play Guidelines
1. **Frequency Limits**:
- No hard limit, but be reasonable
- System may throttle excessive requests
- Cannot check if review was submitted
2. **Requirements**:
- Cannot incentivize reviews
- Must follow Play Store policies
- Review flow must complete in-app
## Setup Guide
### Installation
```bash
npm install native-update
npx cap sync
```
### Android Configuration
No additional configuration required. The plugin automatically includes the Play Core library.
### iOS Configuration
No additional configuration required. The plugin automatically uses StoreKit.
### Capacitor Configuration
```json
{
"plugins": {
"NativeUpdate": {
"appStoreId": "YOUR_APP_STORE_ID",
"reviewPromptDelay": 2000,
"reviewDebugMode": false
}
}
}
```
## Implementation Steps
### Step 1: Basic Implementation
```typescript
import { NativeUpdate } from 'native-update';
export class AppReviewService {
async requestReview() {
try {
const result = await NativeUpdate.requestReview();
if (result.displayed) {
console.log('Review prompt was displayed');
// Track that prompt was shown
await this.analytics.track('review_prompt_displayed');
} else {
console.log('Review prompt was not displayed (system throttled)');
// Maybe try alternative feedback method
await this.showAlternativeFeedback();
}
} catch (error) {
console.error('Review request failed:', error);
// Fallback to external review
await this.openExternalReview();
}
}
}
```
### Step 2: Smart Review Triggers
```typescript
export class SmartReviewManager {
private readonly REVIEW_CONDITIONS = {
minSessions: 5,
minDaysInstalled: 3,
minActionsCompleted: 10,
positiveExperienceRequired: true,
};
async checkAndRequestReview() {
// Check if we should ask for review
const shouldAsk = await this.shouldAskForReview();
if (!shouldAsk) {
return;
}
// Check if user had positive experience
const hasPositiveExperience = await this.checkPositiveExperience();
if (!hasPositiveExperience) {
// Ask for feedback instead
await this.askForFeedback();
return;
}
// Request review
await this.requestReview();
}
private async shouldAskForReview(): Promise<boolean> {
const stats = await this.getUserStats();
// Check all conditions
const conditions = [
stats.sessionCount >= this.REVIEW_CONDITIONS.minSessions,
stats.daysSinceInstall >= this.REVIEW_CONDITIONS.minDaysInstalled,
stats.completedActions >= this.REVIEW_CONDITIONS.minActionsCompleted,
!stats.hasReviewedBefore,
!stats.hasDeclinedRecently,
this.isGoodMoment(),
];
return conditions.every((condition) => condition);
}
private isGoodMoment(): boolean {
// Don't interrupt critical flows
const currentRoute = this.router.url;
const badMoments = ['/checkout', '/payment', '/onboarding', '/support'];
return !badMoments.some((route) => currentRoute.includes(route));
}
private async checkPositiveExperience(): Promise<boolean> {
// Check recent user actions
const recentActions = await this.getRecentUserActions();
const positiveSignals = [
recentActions.includes('task_completed'),
recentActions.includes('content_shared'),
recentActions.includes('milestone_achieved'),
!recentActions.includes('error_encountered'),
!recentActions.includes('support_contacted'),
];
// Need at least 3 positive signals
return positiveSignals.filter((signal) => signal).length >= 3;
}
}
```
### Step 3: Review Trigger Points
```typescript
export class ReviewTriggerPoints {
constructor(
private reviewService: SmartReviewManager,
private analytics: AnalyticsService
) {}
// After completing important action
async onTaskCompleted() {
await this.incrementPositiveAction('task_completed');
// Check if this is a milestone
const taskCount = await this.getCompletedTaskCount();
if (taskCount % 5 === 0) {
// Every 5 tasks
await this.reviewService.checkAndRequestReview();
}
}
// After successful transaction
async onPurchaseCompleted() {
await this.incrementPositiveAction('purchase_completed');
// Wait a bit before asking
setTimeout(() => {
this.reviewService.checkAndRequestReview();
}, 5000);
}
// After achieving milestone
async onMilestoneAchieved(milestone: string) {
await this.incrementPositiveAction('milestone_achieved');
// Show achievement first
await this.showAchievementToast(milestone);
// Then ask for review
setTimeout(() => {
this.reviewService.checkAndRequestReview();
}, 3000);
}
// After positive feedback
async onPositiveFeedback() {
// User already indicated satisfaction
await this.reviewService.requestReview();
}
// App foregrounded (for time-based triggers)
async onAppForegrounded() {
const lastPrompt = await this.getLastReviewPromptDate();
const daysSinceLastPrompt = this.daysSince(lastPrompt);
// Check every 30 days
if (daysSinceLastPrompt >= 30) {
await this.reviewService.checkAndRequestReview();
}
}
}
```
### Step 4: Two-Step Review Flow
```typescript
export class TwoStepReviewFlow {
async initiateReviewFlow() {
// Step 1: Ask if they enjoy the app
const enjoys = await this.askEnjoyment();
if (enjoys) {
// Step 2: Ask for review
await this.askForReview();
} else {
// Ask for feedback instead
await this.askForFeedback();
}
}
private async askEnjoyment(): Promise<boolean> {
return new Promise((resolve) => {
const alert = this.alertController.create({
header: 'Enjoying the app?',
message: 'How has your experience been so far?',
buttons: [
{
text: 'Not really',
handler: () => {
this.analytics.track('review_enjoyment_negative');
resolve(false);
},
},
{
text: 'Yes!',
handler: () => {
this.analytics.track('review_enjoyment_positive');
resolve(true);
},
},
],
});
alert.present();
});
}
private async askForReview() {
const alert = await this.alertController.create({
header: 'Awesome!',
message: 'Would you mind rating us on the app store?',
buttons: [
{
text: 'No thanks',
role: 'cancel',
handler: () => {
this.analytics.track('review_declined');
this.markDeclined();
},
},
{
text: 'Sure!',
handler: async () => {
this.analytics.track('review_accepted');
await NativeUpdate.requestReview();
},
},
],
});
await alert.present();
}
private async askForFeedback() {
const alert = await this.alertController.create({
header: 'We appreciate your feedback',
message: 'What can we do to improve your experience?',
inputs: [
{
name: 'feedback',
type: 'textarea',
placeholder: 'Your feedback...',
},
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Send',
handler: (data) => {
this.submitFeedback(data.feedback);
this.showThankYou();
},
},
],
});
await alert.present();
}
}
```
### Step 5: Alternative Review Methods
```typescript
export class AlternativeReviewMethods {
// For when in-app review is not available
async openExternalReview() {
const platform = Capacitor.getPlatform();
if (platform === 'ios') {
await this.openAppStore();
} else if (platform === 'android') {
await this.openPlayStore();
} else {
await this.openWebReview();
}
}
private async openAppStore() {
const appStoreId = 'YOUR_APP_STORE_ID';
const reviewUrl = `https://apps.apple.com/app/id${appStoreId}?action=write-review`;
try {
await Browser.open({ url: reviewUrl });
this.analytics.track('external_review_opened', { platform: 'ios' });
} catch (error) {
console.error('Failed to open App Store:', error);
}
}
private async openPlayStore() {
const packageName = 'com.example.app';
const playStoreUrl = `https://play.google.com/store/apps/details?id=${packageName}`;
try {
await Browser.open({ url: playStoreUrl });
this.analytics.track('external_review_opened', { platform: 'android' });
} catch (error) {
console.error('Failed to open Play Store:', error);
}
}
private async openWebReview() {
// Custom web review form or third-party service
const modal = await this.modalController.create({
component: WebReviewComponent,
});
await modal.present();
}
}
```
## Best Practices
### 1. Timing is Everything
```typescript
export class ReviewTimingStrategy {
// Good moments to ask
private readonly GOOD_MOMENTS = [
'after_milestone_achieved',
'after_successful_transaction',
'after_positive_interaction',
'after_content_shared',
'after_returning_user_session',
];
// Bad moments to avoid
private readonly BAD_MOMENTS = [
'during_onboarding',
'after_error',
'during_payment_flow',
'immediately_after_install',
'after_support_contact',
'during_critical_task',
];
async isGoodMomentForReview(): Promise<boolean> {
const currentContext = await this.getCurrentUserContext();
// Check if it's a bad moment
if (this.BAD_MOMENTS.includes(currentContext.state)) {
return false;
}
// Check if it's explicitly a good moment
if (this.GOOD_MOMENTS.includes(currentContext.state)) {
return true;
}
// Additional checks
return this.additionalTimingChecks(currentContext);
}
private async additionalTimingChecks(context: any): Promise<boolean> {
// Don't ask too soon after install
if (context.daysSinceInstall < 3) return false;
// Don't ask if user is in a hurry
if (context.sessionDuration < 60) return false;
// Don't ask if user had recent issues
if (context.recentErrors > 0) return false;
return true;
}
}
```
### 2. Respect User Choice
```typescript
export class ReviewPreferenceManager {
private readonly PREFERENCE_KEY = 'review_preferences';
async userDeclinedReview() {
const prefs = await this.getPreferences();
prefs.declineCount++;
prefs.lastDeclineDate = new Date().toISOString();
// After 3 declines, stop asking
if (prefs.declineCount >= 3) {
prefs.permanentlyDeclined = true;
}
await this.savePreferences(prefs);
}
async canAskForReview(): Promise<boolean> {
const prefs = await this.getPreferences();
// Never ask if permanently declined
if (prefs.permanentlyDeclined) return false;
// Wait longer after each decline
const daysSinceDecline = this.daysSince(prefs.lastDeclineDate);
const requiredDays = prefs.declineCount * 30; // 30, 60, 90 days
return daysSinceDecline >= requiredDays;
}
async resetAfterPositiveReview() {
const prefs = await this.getPreferences();
prefs.hasReviewed = true;
prefs.reviewDate = new Date().toISOString();
prefs.declineCount = 0;
await this.savePreferences(prefs);
}
}
```
### 3. A/B Testing
```typescript
export class ReviewABTesting {
async getReviewStrategy(): Promise<string> {
const userId = await this.getUserId();
const variant = this.hashUserId(userId) % 3;
switch (variant) {
case 0:
return 'immediate'; // Direct review request
case 1:
return 'two-step'; // Ask enjoyment first
case 2:
return 'contextual'; // Wait for positive moment
}
}
async executeStrategy(strategy: string) {
this.analytics.track('review_strategy_assigned', { strategy });
switch (strategy) {
case 'immediate':
await NativeUpdate.requestReview();
break;
case 'two-step':
await this.twoStepFlow.initiateReviewFlow();
break;
case 'contextual':
await this.contextualFlow.waitForPositiveMoment();
break;
}
}
}
```
## Analytics Integration
### Track Everything
```typescript
export class ReviewAnalytics {
private events = {
// Prompt events
PROMPT_TRIGGERED: 'review_prompt_triggered',
PROMPT_DISPLAYED: 'review_prompt_displayed',
PROMPT_DISMISSED: 'review_prompt_dismissed',
// User actions
REVIEW_ACCEPTED: 'review_accepted',
REVIEW_DECLINED: 'review_declined',
REVIEW_COMPLETED: 'review_completed', // Best guess
// Feedback events
FEEDBACK_PROVIDED: 'feedback_provided',
EXTERNAL_REVIEW: 'external_review_opened',
};
async trackReviewFlow(stage: string, properties?: any) {
await this.analytics.track(this.events[stage], {
...properties,
timestamp: new Date().toISOString(),
sessionId: this.sessionId,
userId: this.userId,
platform: Capacitor.getPlatform(),
appVersion: this.appVersion,
});
}
async generateReviewReport(): Promise<ReviewMetrics> {
const metrics = await this.analytics.query({
events: Object.values(this.events),
timeframe: 'last_30_days',
});
return {
promptsShown: metrics[this.events.PROMPT_DISPLAYED],
acceptanceRate:
metrics[this.events.REVIEW_ACCEPTED] /
metrics[this.events.PROMPT_DISPLAYED],
declineRate:
metrics[this.events.REVIEW_DECLINED] /
metrics[this.events.PROMPT_DISPLAYED],
estimatedCompletionRate: this.estimateCompletionRate(metrics),
feedbackRate:
metrics[this.events.FEEDBACK_PROVIDED] /
metrics[this.events.PROMPT_DISPLAYED],
};
}
}
```
## Testing
### Development Testing
```typescript
export class ReviewTestingUtils {
async enableTestMode() {
// Enable debug mode
await NativeUpdate.setReviewDebugMode({ enabled: true });
// Reset all preferences
await this.clearReviewPreferences();
// Set up test conditions
await this.setupTestConditions();
}
async simulateReviewFlow() {
console.log('Simulating review flow...');
// Force display of review prompt
const result = await NativeUpdate.requestReview({
force: true, // Only works in debug mode
});
console.log('Review simulation result:', result);
}
async testDifferentScenarios() {
const scenarios = [
{ name: 'First time user', daysSinceInstall: 0 },
{ name: 'Happy user', positiveActions: 10 },
{ name: 'Frustrated user', errors: 5 },
{ name: 'Returning user', sessions: 20 },
];
for (const scenario of scenarios) {
await this.setupScenario(scenario);
const shouldShow = await this.reviewManager.shouldAskForReview();
console.log(
`Scenario "${scenario.name}": ${shouldShow ? 'SHOW' : 'HIDE'}`
);
}
}
}
```
### Platform-Specific Testing
#### iOS Testing
1. Use development build (not TestFlight)
2. Reviews work in debug mode
3. Can test multiple times
4. Check console for SKStoreReviewController logs
#### Android Testing
1. Use internal test track
2. Sign in with test account
3. Clear Play Store cache between tests
4. Check Play Console for metrics
## Troubleshooting
### Common Issues
#### 1. Review Prompt Not Showing
```typescript
// Debug checklist
async function debugReviewPrompt() {
// Check if available on platform
const isAvailable = await NativeUpdate.isReviewAvailable();
console.log('Review available:', isAvailable);
// Check system throttling
const debugInfo = await NativeUpdate.getReviewDebugInfo();
console.log('Debug info:', debugInfo);
// Check your conditions
const conditions = await this.checkAllConditions();
console.log('App conditions:', conditions);
}
```
#### 2. Low Review Rates
```typescript
// Optimize review strategy
class ReviewOptimizer {
async analyzeAndOptimize() {
const metrics = await this.getReviewMetrics();
if (metrics.promptsShown < 100) {
console.log('Not enough data yet');
return;
}
if (metrics.acceptanceRate < 0.1) {
// Less than 10% accepting
console.log('Consider:');
console.log('- Improving timing');
console.log('- Using two-step flow');
console.log('- Checking for negative experiences');
}
if (metrics.promptsShown / metrics.eligibleUsers < 0.5) {
console.log('Not reaching enough users');
console.log('- Relax conditions');
console.log('- Add more trigger points');
}
}
}
```
#### 3. Platform-Specific Issues
```typescript
// Platform fallbacks
async function requestReviewWithFallback() {
try {
const result = await NativeUpdate.requestReview();
if (!result.displayed) {
// System didn't show prompt
if (Capacitor.getPlatform() === 'web') {
await this.showWebReviewUI();
} else {
// Try again later
await this.scheduleRetry();
}
}
} catch (error) {
// API not available
await this.openExternalReview();
}
}
```
## Summary
Key takeaways for implementing app reviews:
1. **Time it right** - Ask when users are happy
2. **Be respectful** - Don't ask too often
3. **Track everything** - Measure and optimize
4. **Handle all cases** - Have fallbacks ready
5. **Follow guidelines** - Respect platform rules
6. **Test thoroughly** - Use debug modes
7. **Iterate** - Continuously improve based on data
## Next Steps
- Review the [Quick Start Guide](./QUICK_START.md)
- Check the [API Reference](./api/app-review-api.md)
- See example implementation in the `/example` directory