UNPKG

capacitor-native-update

Version:
976 lines (789 loc) 23.8 kB
# App Reviews The App Reviews feature provides intelligent in-app review prompts that help you gather user feedback without interrupting the user experience. It integrates with native store review systems and includes smart timing algorithms to maximize positive reviews. ## Overview App reviews are crucial for app store visibility and user trust. This feature helps you: - Request reviews at optimal moments - Avoid review fatigue and spam - Comply with platform guidelines - Maximize positive review rates ## Platform Integration ### iOS - StoreKit - Uses `SKStoreReviewController` for native review prompts - Automatically handles App Store guidelines - No user redirection required ### Android - Play Core - Uses Google Play Core Library for in-app reviews - Seamless integration with Play Store - Follows Google Play policies ### Web - Fallback - Custom review prompts - Redirects to appropriate stores - Graceful degradation ## Key Features ### 🎯 Smart Timing - Analyzes user behavior patterns - Triggers after positive interactions - Respects platform limitations ### 📊 Review Intelligence - Tracks review history - Prevents over-prompting - Optimizes timing based on user segments ### 🔧 Customizable Rules - Flexible triggering conditions - Custom event tracking - A/B testing support ### 🚀 Platform Compliance - Follows App Store guidelines - Respects Play Store policies - Handles platform restrictions ## Implementation Guide ### Basic Implementation ```typescript // Configure app reviews await CapacitorNativeUpdate.configure({ appReview: { minimumDaysSinceInstall: 7, minimumDaysSinceLastPrompt: 60, minimumLaunchCount: 3, }, }); // Request review at appropriate moment async function requestReview() { try { // Check if we can request a review const canRequest = await CapacitorNativeUpdate.AppReview.canRequestReview(); if (canRequest.allowed) { // Request the review const result = await CapacitorNativeUpdate.AppReview.requestReview(); if (result.shown) { console.log('Review dialog was shown'); } else { console.log('Review dialog not shown:', result.reason); } } else { console.log('Cannot request review:', canRequest.reason); } } catch (error) { console.error('Review request failed:', error); } } ``` ### Advanced Implementation ```typescript class AppReviewManager { private userEvents: Map<string, number> = new Map(); private positiveActions: string[] = []; async initialize() { // Configure with advanced settings await CapacitorNativeUpdate.configure({ appReview: { minimumDaysSinceInstall: 14, minimumDaysSinceLastPrompt: 90, minimumLaunchCount: 5, minimumSignificantEvents: 3, customTriggers: [ 'purchase_completed', 'level_completed', 'task_finished', 'positive_feedback', ], requirePositiveEvents: true, maxPromptsPerVersion: 1, }, }); // Set up event tracking this.setupEventTracking(); } private setupEventTracking() { // Track app launches this.trackEvent('app_launch'); // Track positive user actions this.trackPositiveAction('app_opened'); } // Track custom events trackEvent(eventName: string) { const count = this.userEvents.get(eventName) || 0; this.userEvents.set(eventName, count + 1); // Check if this event should trigger review this.checkReviewTrigger(eventName); } // Track positive user actions trackPositiveAction(action: string) { this.positiveActions.push(action); this.trackEvent(action); } private async checkReviewTrigger(eventName: string) { // Check if this event is a review trigger const triggers = [ 'purchase_completed', 'level_completed', 'task_finished', 'positive_feedback', ]; if (triggers.includes(eventName)) { // Add delay to avoid interrupting user flow setTimeout(() => { this.considerReviewRequest(); }, 2000); } } private async considerReviewRequest() { try { // Check if review is appropriate const analysis = await this.analyzeReviewReadiness(); if (analysis.shouldRequest) { await this.requestReview(); } else { console.log('Review not requested:', analysis.reason); } } catch (error) { console.error('Review analysis failed:', error); } } private async analyzeReviewReadiness() { // Check basic eligibility const canRequest = await CapacitorNativeUpdate.AppReview.canRequestReview(); if (!canRequest.allowed) { return { shouldRequest: false, reason: canRequest.reason, }; } // Check user sentiment const sentiment = this.analyzeUserSentiment(); if (sentiment.score < 0.7) { return { shouldRequest: false, reason: 'User sentiment too low', }; } // Check recent app usage const usage = await this.analyzeAppUsage(); if (!usage.isActive) { return { shouldRequest: false, reason: 'User not actively using app', }; } return { shouldRequest: true, reason: 'All conditions met', }; } private analyzeUserSentiment() { // Simple sentiment analysis based on actions const positiveWeight = this.positiveActions.length * 0.3; const negativeWeight = this.getNegativeActions().length * 0.7; const score = Math.max(0, Math.min(1, positiveWeight - negativeWeight)); return { score, confidence: Math.min(1, this.positiveActions.length / 10), }; } private async analyzeAppUsage() { const launchCount = this.userEvents.get('app_launch') || 0; const sessionDuration = await this.getAverageSessionDuration(); return { isActive: launchCount >= 5 && sessionDuration > 120000, // 2 minutes engagement: Math.min(1, launchCount / 20), retention: await this.calculateRetention(), }; } async requestReview() { try { const result = await CapacitorNativeUpdate.AppReview.requestReview(); // Track the request this.trackReviewRequest(result); if (result.shown) { // Review dialog was shown console.log('Review prompt shown successfully'); // Optional: Show thank you message setTimeout(() => { this.showThankYouMessage(); }, 5000); } return result; } catch (error) { console.error('Review request failed:', error); throw error; } } private trackReviewRequest(result: ReviewResult) { // Track analytics this.trackEvent('review_requested'); if (result.shown) { this.trackEvent('review_shown'); } // Log for debugging console.log('Review request result:', result); } } ``` ## Smart Timing Strategies ### Optimal Moments ```typescript class ReviewTimingOptimizer { // After successful task completion async onTaskCompleted(taskType: string) { const isPositive = await this.isPositiveTask(taskType); if (isPositive) { // Wait for user to feel satisfaction setTimeout(() => { this.considerReview('task_completion'); }, 1500); } } // After positive app interaction async onPositiveInteraction(interaction: string) { const interactions = [ 'feature_liked', 'content_shared', 'positive_rating_given', 'upgrade_purchased', ]; if (interactions.includes(interaction)) { this.considerReview('positive_interaction'); } } // After user shows engagement async onEngagementMilestone(milestone: string) { const milestones = { daily_streak_7: 0.8, goals_completed_10: 0.9, features_explored_5: 0.7, time_spent_milestone: 0.6, }; const probability = milestones[milestone]; if (probability && Math.random() < probability) { this.considerReview('engagement_milestone'); } } private async considerReview(trigger: string) { // Analyze context const context = await this.analyzeContext(); if (context.appropriate) { // Small delay to avoid interrupting user flow setTimeout(() => { this.requestReview(); }, 2000); } } private async analyzeContext() { // Check if user is in a good state const checks = [ this.isUserInGoodMood(), this.isAppPerformingWell(), this.isFeatureWorkingCorrectly(), this.hasUserTimeForReview(), ]; const results = await Promise.all(checks); return { appropriate: results.every((check) => check), confidence: results.filter((check) => check).length / results.length, }; } } ``` ### Avoiding Review Fatigue ```typescript class ReviewFatigueManager { private reviewHistory: ReviewAttempt[] = []; async canRequestReview(): Promise<boolean> { // Check platform limits const platformCheck = await CapacitorNativeUpdate.AppReview.canRequestReview(); if (!platformCheck.allowed) { return false; } // Check our custom rules const customChecks = [ this.checkFrequencyLimit(), this.checkUserResponseHistory(), this.checkAppVersionHistory(), this.checkUserSegment(), ]; return customChecks.every((check) => check); } private checkFrequencyLimit(): boolean { const lastRequest = this.getLastReviewRequest(); if (!lastRequest) return true; const daysSinceLastRequest = this.daysSince(lastRequest.timestamp); const minimumDays = this.getMinimumDaysBetweenRequests(); return daysSinceLastRequest >= minimumDays; } private checkUserResponseHistory(): boolean { const recentResponses = this.reviewHistory .filter((attempt) => this.daysSince(attempt.timestamp) <= 180) .map((attempt) => attempt.response); // If user dismissed last 2 requests, wait longer const recentDismissals = recentResponses.filter( (r) => r === 'dismissed' ).length; return recentDismissals < 2; } private getMinimumDaysBetweenRequests(): number { const baseMinimum = 60; // 60 days base const dismissalPenalty = this.getDismissalPenalty(); return baseMinimum + dismissalPenalty; } private getDismissalPenalty(): number { // Increase delay for users who dismiss frequently const dismissals = this.reviewHistory.filter( (r) => r.response === 'dismissed' ).length; return Math.min(dismissals * 30, 180); // Max 180 days penalty } } ``` ## Custom Review Triggers ### Event-Based Triggers ```typescript class CustomReviewTriggers { private triggerEvents: Map<string, TriggerConfig> = new Map(); setupCustomTriggers() { // E-commerce triggers this.triggerEvents.set('purchase_completed', { weight: 0.9, delay: 3000, conditions: ['payment_successful', 'no_recent_issues'], }); // Gaming triggers this.triggerEvents.set('level_completed', { weight: 0.7, delay: 2000, conditions: ['level_difficulty_high', 'no_deaths'], }); // Productivity triggers this.triggerEvents.set('goal_achieved', { weight: 0.8, delay: 1500, conditions: ['streak_active', 'feature_used_recently'], }); // Social triggers this.triggerEvents.set('content_shared', { weight: 0.6, delay: 5000, conditions: ['sharing_successful', 'positive_engagement'], }); } async handleTriggerEvent(eventName: string, metadata: any) { const config = this.triggerEvents.get(eventName); if (!config) return; // Check conditions const conditionsMet = await this.checkConditions( config.conditions, metadata ); if (!conditionsMet) { console.log(`Conditions not met for trigger: ${eventName}`); return; } // Apply probability if (Math.random() > config.weight) { console.log(`Probability check failed for trigger: ${eventName}`); return; } // Add delay setTimeout(() => { this.requestReview(`trigger_${eventName}`); }, config.delay); } private async checkConditions( conditions: string[], metadata: any ): Promise<boolean> { const conditionChecks = { payment_successful: () => metadata.paymentStatus === 'success', no_recent_issues: () => !this.hasRecentIssues(), level_difficulty_high: () => metadata.difficulty >= 3, no_deaths: () => metadata.deaths === 0, streak_active: () => this.getUserStreak() > 0, feature_used_recently: () => this.wasFeatureUsedRecently(), sharing_successful: () => metadata.shareResult === 'success', positive_engagement: () => metadata.likes > 0, }; return conditions.every((condition) => { const check = conditionChecks[condition]; return check ? check() : false; }); } } ``` ### Behavioral Triggers ```typescript class BehavioralTriggers { private behaviorTracker = new Map<string, number>(); // Track user behavior patterns trackBehavior(behavior: string, value: number = 1) { const current = this.behaviorTracker.get(behavior) || 0; this.behaviorTracker.set(behavior, current + value); this.checkBehavioralTriggers(behavior); } private checkBehavioralTriggers(behavior: string) { const triggers = { app_launches: { threshold: 10, probability: 0.3, }, feature_usage: { threshold: 15, probability: 0.6, }, session_duration: { threshold: 1800, // 30 minutes probability: 0.8, }, content_created: { threshold: 5, probability: 0.9, }, }; const config = triggers[behavior]; if (!config) return; const currentValue = this.behaviorTracker.get(behavior) || 0; if ( currentValue >= config.threshold && Math.random() < config.probability ) { this.requestReview(`behavioral_${behavior}`); } } // Advanced behavioral analysis async analyzeBehavioralPatterns() { const patterns = { engagement: this.calculateEngagementScore(), retention: await this.calculateRetentionRate(), satisfaction: this.calculateSatisfactionScore(), growth: this.calculateGrowthMetrics(), }; // Determine if user is in a good state for review const overallScore = patterns.engagement * 0.3 + patterns.retention * 0.2 + patterns.satisfaction * 0.4 + patterns.growth * 0.1; return { score: overallScore, recommendReview: overallScore > 0.7, patterns, }; } } ``` ## Platform-Specific Implementation ### iOS StoreKit Integration ```typescript // iOS-specific review handling class iOSReviewManager { async requestReview(): Promise<ReviewResult> { try { // Use StoreKit review controller const result = await CapacitorNativeUpdate.AppReview.requestReview(); // iOS handles everything natively return { shown: result.shown, platform: 'ios', method: 'storekit', }; } catch (error) { console.error('iOS review request failed:', error); // Fallback to App Store redirect return this.fallbackToAppStore(); } } private async fallbackToAppStore() { // Open App Store page await CapacitorNativeUpdate.AppUpdate.openAppStore(); return { shown: true, platform: 'ios', method: 'appstore_redirect', }; } } ``` ### Android Play Core Integration ```typescript // Android-specific review handling class AndroidReviewManager { async requestReview(): Promise<ReviewResult> { try { // Use Play Core in-app review const result = await CapacitorNativeUpdate.AppReview.requestReview(); return { shown: result.shown, platform: 'android', method: 'play_core', }; } catch (error) { console.error('Android review request failed:', error); // Fallback to Play Store return this.fallbackToPlayStore(); } } private async fallbackToPlayStore() { // Open Play Store page await CapacitorNativeUpdate.AppUpdate.openAppStore(); return { shown: true, platform: 'android', method: 'playstore_redirect', }; } } ``` ### Web Platform Fallback ```typescript // Web platform review handling class WebReviewManager { async requestReview(): Promise<ReviewResult> { // Show custom review dialog const userChoice = await this.showCustomReviewDialog(); if (userChoice === 'review') { // Redirect to appropriate store this.redirectToStore(); } return { shown: true, platform: 'web', method: 'custom_dialog', userChoice, }; } private async showCustomReviewDialog() { return new Promise((resolve) => { // Create custom modal const modal = this.createReviewModal(); modal.onChoice = (choice) => { modal.close(); resolve(choice); }; modal.show(); }); } private redirectToStore() { const platform = this.detectPlatform(); const urls = { ios: 'https://apps.apple.com/app/id123456789', android: 'https://play.google.com/store/apps/details?id=com.myapp', web: 'https://myapp.com/feedback', }; window.open(urls[platform] || urls.web, '_blank'); } } ``` ## Analytics and Monitoring ### Review Metrics Tracking ```typescript class ReviewAnalytics { async trackReviewMetrics(event: string, data: any) { const metrics = { timestamp: Date.now(), event, ...data, userSegment: await this.getUserSegment(), appVersion: await this.getAppVersion(), platform: this.getPlatform(), }; // Send to analytics service await this.analytics.track('app_review_' + event, metrics); } // Track review funnel async trackReviewFunnel(step: string, success: boolean) { const funnelData = { step, success, timestamp: Date.now(), sessionId: this.getSessionId(), }; await this.trackReviewMetrics('funnel_step', funnelData); } // Monitor review performance async getReviewPerformance() { const metrics = await this.analytics.query({ events: [ 'app_review_requested', 'app_review_shown', 'app_review_completed', ], timeRange: '30d', }); return { requestRate: metrics.requested / metrics.eligible, showRate: metrics.shown / metrics.requested, completionRate: metrics.completed / metrics.shown, overallConversion: metrics.completed / metrics.eligible, }; } } ``` ### A/B Testing Review Strategies ```typescript class ReviewABTesting { private experiments = new Map<string, ABTest>(); async setupReviewExperiment( experimentName: string, variations: ABVariation[] ) { const experiment = { name: experimentName, variations, allocation: this.allocateUsers(variations), metrics: ['show_rate', 'completion_rate', 'user_satisfaction'], }; this.experiments.set(experimentName, experiment); } async getReviewStrategy(userId: string): Promise<ReviewStrategy> { const experiment = this.experiments.get('review_timing'); if (!experiment) { return this.getDefaultStrategy(); } const variation = this.getUserVariation(userId, experiment); return { timing: variation.timing, frequency: variation.frequency, triggers: variation.triggers, ui: variation.ui, }; } private getUserVariation(userId: string, experiment: ABTest): ABVariation { const hash = this.hashUserId(userId); const bucket = hash % 100; let cumulative = 0; for (const variation of experiment.variations) { cumulative += variation.allocation; if (bucket < cumulative) { return variation; } } return experiment.variations[0]; // Fallback } } ``` ## Best Practices ### 1. Respect User Experience ```typescript // Never interrupt critical user flows class UserFlowProtection { private criticalFlows = [ 'payment_processing', 'form_submission', 'content_creation', 'authentication', ]; async isUserInCriticalFlow(): Promise<boolean> { const currentFlow = await this.getCurrentUserFlow(); return this.criticalFlows.includes(currentFlow); } async requestReviewSafely() { if (await this.isUserInCriticalFlow()) { // Schedule for later this.scheduleReviewForLater(); return; } // Safe to request review await this.requestReview(); } } ``` ### 2. Personalize Review Requests ```typescript // Tailor review requests to user segments class PersonalizedReviews { async getPersonalizedMessage(user: User): Promise<string> { const segment = await this.getUserSegment(user); const messages = { new_user: "You've been using our app for a week now. How's your experience?", power_user: "You're one of our most active users! Would you mind sharing your thoughts?", premium_user: 'As a premium member, your feedback is invaluable to us.', casual_user: "Hope you're enjoying the app! A quick review would help us improve.", }; return messages[segment] || messages['casual_user']; } } ``` ### 3. Handle Edge Cases ```typescript // Handle various edge cases gracefully class EdgeCaseHandler { async handleReviewRequest() { try { // Check for edge cases if (await this.isAppInBackground()) { return { shown: false, reason: 'App in background' }; } if (await this.isDeviceInDoNotDisturb()) { return { shown: false, reason: 'Do not disturb mode' }; } if (await this.isUserInCall()) { return { shown: false, reason: 'User in call' }; } // Normal review request return await this.requestReview(); } catch (error) { console.error('Review request failed:', error); return { shown: false, reason: 'Error occurred' }; } } } ``` ## Testing Review Flows ### Development Testing ```typescript // Enable debug mode for testing const debugConfig = { appReview: { debugMode: true, // Bypass all time restrictions minimumDaysSinceInstall: 0, minimumDaysSinceLastPrompt: 0, minimumLaunchCount: 0, }, }; // Test different scenarios async function testReviewScenarios() { // Test immediate request await CapacitorNativeUpdate.AppReview.requestReview(); // Test eligibility check const canRequest = await CapacitorNativeUpdate.AppReview.canRequestReview(); console.log('Can request review:', canRequest); // Test custom triggers await testCustomTriggers(); } ``` ### User Testing ```typescript // A/B test different review strategies const strategies = [ { name: 'aggressive', config: { minimumDaysSinceInstall: 3, minimumLaunchCount: 2 }, }, { name: 'balanced', config: { minimumDaysSinceInstall: 7, minimumLaunchCount: 5 }, }, { name: 'conservative', config: { minimumDaysSinceInstall: 14, minimumLaunchCount: 10 }, }, ]; // Monitor success rates async function monitorReviewSuccess() { const metrics = await analytics.getReviewMetrics(); return { showRate: metrics.shown / metrics.requested, completionRate: metrics.completed / metrics.shown, userSatisfaction: metrics.positiveRatings / metrics.totalRatings, }; } ``` ## Next Steps - Implement [Security Best Practices](../guides/security-best-practices.md) - Set up [Analytics Tracking](../examples/analytics-integration.md) - Configure [Live Updates](./live-updates.md) - Review [API Reference](../api/app-review-api.md) --- Made with ❤️ by Ahsan Mahmood