native-update 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorNativeUpdate.podspec +18 -0
- package/LICENSE +21 -0
- package/Readme.md +451 -0
- package/android/build.gradle +92 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/android/gradle.properties +17 -0
- package/android/proguard-rules.pro +29 -0
- package/android/settings.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +34 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppReviewPlugin.kt +153 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt +275 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +390 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateManager.kt +46 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt +333 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateWorker.kt +251 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/CapacitorNativeUpdatePlugin.kt +265 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +526 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +99 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt +249 -0
- package/dist/esm/__tests__/bundle-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/bundle-manager.test.js +123 -0
- package/dist/esm/__tests__/bundle-manager.test.js.map +1 -0
- package/dist/esm/__tests__/config.test.d.ts +1 -0
- package/dist/esm/__tests__/config.test.js +69 -0
- package/dist/esm/__tests__/config.test.js.map +1 -0
- package/dist/esm/__tests__/integration.test.d.ts +1 -0
- package/dist/esm/__tests__/integration.test.js +78 -0
- package/dist/esm/__tests__/integration.test.js.map +1 -0
- package/dist/esm/__tests__/security.test.d.ts +1 -0
- package/dist/esm/__tests__/security.test.js +54 -0
- package/dist/esm/__tests__/security.test.js.map +1 -0
- package/dist/esm/__tests__/version-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/version-manager.test.js +45 -0
- package/dist/esm/__tests__/version-manager.test.js.map +1 -0
- package/dist/esm/app-review/app-review-manager.d.ts +24 -0
- package/dist/esm/app-review/app-review-manager.js +195 -0
- package/dist/esm/app-review/app-review-manager.js.map +1 -0
- package/dist/esm/app-review/index.d.ts +5 -0
- package/dist/esm/app-review/index.js +6 -0
- package/dist/esm/app-review/index.js.map +1 -0
- package/dist/esm/app-review/platform-review-handler.d.ts +20 -0
- package/dist/esm/app-review/platform-review-handler.js +138 -0
- package/dist/esm/app-review/platform-review-handler.js.map +1 -0
- package/dist/esm/app-review/review-conditions-checker.d.ts +22 -0
- package/dist/esm/app-review/review-conditions-checker.js +155 -0
- package/dist/esm/app-review/review-conditions-checker.js.map +1 -0
- package/dist/esm/app-review/review-rate-limiter.d.ts +23 -0
- package/dist/esm/app-review/review-rate-limiter.js +164 -0
- package/dist/esm/app-review/review-rate-limiter.js.map +1 -0
- package/dist/esm/app-review/types.d.ts +41 -0
- package/dist/esm/app-review/types.js +2 -0
- package/dist/esm/app-review/types.js.map +1 -0
- package/dist/esm/app-update/app-update-checker.d.ts +13 -0
- package/dist/esm/app-update/app-update-checker.js +104 -0
- package/dist/esm/app-update/app-update-checker.js.map +1 -0
- package/dist/esm/app-update/app-update-installer.d.ts +19 -0
- package/dist/esm/app-update/app-update-installer.js +123 -0
- package/dist/esm/app-update/app-update-installer.js.map +1 -0
- package/dist/esm/app-update/app-update-manager.d.ts +28 -0
- package/dist/esm/app-update/app-update-manager.js +199 -0
- package/dist/esm/app-update/app-update-manager.js.map +1 -0
- package/dist/esm/app-update/app-update-notifier.d.ts +14 -0
- package/dist/esm/app-update/app-update-notifier.js +100 -0
- package/dist/esm/app-update/app-update-notifier.js.map +1 -0
- package/dist/esm/app-update/index.d.ts +6 -0
- package/dist/esm/app-update/index.js +7 -0
- package/dist/esm/app-update/index.js.map +1 -0
- package/dist/esm/app-update/platform-app-update.d.ts +19 -0
- package/dist/esm/app-update/platform-app-update.js +129 -0
- package/dist/esm/app-update/platform-app-update.js.map +1 -0
- package/dist/esm/app-update/types.d.ts +58 -0
- package/dist/esm/app-update/types.js +12 -0
- package/dist/esm/app-update/types.js.map +1 -0
- package/dist/esm/background-update/background-scheduler.d.ts +17 -0
- package/dist/esm/background-update/background-scheduler.js +195 -0
- package/dist/esm/background-update/background-scheduler.js.map +1 -0
- package/dist/esm/background-update/index.d.ts +3 -0
- package/dist/esm/background-update/index.js +3 -0
- package/dist/esm/background-update/index.js.map +1 -0
- package/dist/esm/background-update/notification-manager.d.ts +29 -0
- package/dist/esm/background-update/notification-manager.js +89 -0
- package/dist/esm/background-update/notification-manager.js.map +1 -0
- package/dist/esm/core/analytics.d.ts +70 -0
- package/dist/esm/core/analytics.js +137 -0
- package/dist/esm/core/analytics.js.map +1 -0
- package/dist/esm/core/cache-manager.d.ts +72 -0
- package/dist/esm/core/cache-manager.js +275 -0
- package/dist/esm/core/cache-manager.js.map +1 -0
- package/dist/esm/core/config.d.ts +48 -0
- package/dist/esm/core/config.js +83 -0
- package/dist/esm/core/config.js.map +1 -0
- package/dist/esm/core/errors.d.ts +51 -0
- package/dist/esm/core/errors.js +80 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/logger.d.ts +21 -0
- package/dist/esm/core/logger.js +109 -0
- package/dist/esm/core/logger.js.map +1 -0
- package/dist/esm/core/performance.d.ts +53 -0
- package/dist/esm/core/performance.js +140 -0
- package/dist/esm/core/performance.js.map +1 -0
- package/dist/esm/core/plugin-manager.d.ts +66 -0
- package/dist/esm/core/plugin-manager.js +148 -0
- package/dist/esm/core/plugin-manager.js.map +1 -0
- package/dist/esm/core/security.d.ts +93 -0
- package/dist/esm/core/security.js +315 -0
- package/dist/esm/core/security.js.map +1 -0
- package/dist/esm/definitions.d.ts +639 -0
- package/dist/esm/definitions.js +103 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/live-update/bundle-manager.d.ts +94 -0
- package/dist/esm/live-update/bundle-manager.js +310 -0
- package/dist/esm/live-update/bundle-manager.js.map +1 -0
- package/dist/esm/live-update/certificate-pinning.d.ts +38 -0
- package/dist/esm/live-update/certificate-pinning.js +78 -0
- package/dist/esm/live-update/certificate-pinning.js.map +1 -0
- package/dist/esm/live-update/download-manager.d.ts +67 -0
- package/dist/esm/live-update/download-manager.js +319 -0
- package/dist/esm/live-update/download-manager.js.map +1 -0
- package/dist/esm/live-update/update-manager.d.ts +52 -0
- package/dist/esm/live-update/update-manager.js +294 -0
- package/dist/esm/live-update/update-manager.js.map +1 -0
- package/dist/esm/live-update/version-manager.d.ts +84 -0
- package/dist/esm/live-update/version-manager.js +335 -0
- package/dist/esm/live-update/version-manager.js.map +1 -0
- package/dist/esm/plugin.d.ts +6 -0
- package/dist/esm/plugin.js +283 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/security/crypto.d.ts +25 -0
- package/dist/esm/security/crypto.js +70 -0
- package/dist/esm/security/crypto.js.map +1 -0
- package/dist/esm/security/validator.d.ts +60 -0
- package/dist/esm/security/validator.js +143 -0
- package/dist/esm/security/validator.js.map +1 -0
- package/dist/esm/web.d.ts +74 -0
- package/dist/esm/web.js +595 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +2 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.esm.js +2 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -0
- package/docs/APP_REVIEW_GUIDE.md +768 -0
- package/docs/BUNDLE_SIGNING.md +264 -0
- package/docs/LIVE_UPDATES_GUIDE.md +650 -0
- package/docs/MIGRATION.md +192 -0
- package/docs/NATIVE_UPDATES_GUIDE.md +694 -0
- package/docs/QUICK_START.md +606 -0
- package/docs/README.md +111 -0
- package/docs/REMAINING_FEATURES.md +139 -0
- package/docs/api/app-review-api.md +259 -0
- package/docs/api/app-update-api.md +238 -0
- package/docs/api/events-api.md +451 -0
- package/docs/api/live-update-api.md +265 -0
- package/docs/background-updates.md +392 -0
- package/docs/examples/advanced-scenarios.md +410 -0
- package/docs/examples/basic-usage.md +185 -0
- package/docs/features/app-reviews.md +975 -0
- package/docs/features/app-updates.md +785 -0
- package/docs/features/live-updates.md +633 -0
- package/docs/getting-started/configuration.md +468 -0
- package/docs/getting-started/installation.md +209 -0
- package/docs/getting-started/quick-start.md +379 -0
- package/docs/guides/deployment-guide.md +333 -0
- package/docs/guides/migration-from-codepush.md +142 -0
- package/docs/guides/security-best-practices.md +1057 -0
- package/docs/guides/testing-guide.md +373 -0
- package/docs/production-readiness.md +478 -0
- package/docs/security/certificate-pinning.md +122 -0
- package/docs/server-requirements.md +147 -0
- package/ios/Plugin/AppReview/AppReviewPlugin.swift +158 -0
- package/ios/Plugin/AppUpdate/AppUpdatePlugin.swift +234 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundNotificationManager.swift +329 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +396 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.m +45 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.swift +190 -0
- package/ios/Plugin/Info.plist +43 -0
- package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +689 -0
- package/ios/Plugin/LiveUpdate/WebViewConfiguration.swift +45 -0
- package/ios/Plugin/Security/SecurityManager.swift +289 -0
- package/package.json +90 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
# App Review Implementation Guide
|
|
2
|
+
|
|
3
|
+
This comprehensive guide explains how to implement in-app review functionality in your Capacitor application using the CapacitorNativeUpdate plugin.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Why In-App Reviews Matter](#why-in-app-reviews-matter)
|
|
9
|
+
- [Platform Guidelines](#platform-guidelines)
|
|
10
|
+
- [Setup Guide](#setup-guide)
|
|
11
|
+
- [Implementation Steps](#implementation-steps)
|
|
12
|
+
- [Best Practices](#best-practices)
|
|
13
|
+
- [Analytics Integration](#analytics-integration)
|
|
14
|
+
- [Testing](#testing)
|
|
15
|
+
- [Troubleshooting](#troubleshooting)
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
The App Review feature allows users to rate and review your app without leaving it, significantly improving the likelihood of receiving feedback.
|
|
20
|
+
|
|
21
|
+
### Key Benefits
|
|
22
|
+
|
|
23
|
+
- 📈 **Higher Review Rates**: 2-3x more reviews than traditional methods
|
|
24
|
+
- 🎯 **Better Timing**: Ask for reviews at optimal moments
|
|
25
|
+
- 😊 **Improved UX**: No app switching required
|
|
26
|
+
- 📊 **Better Ratings**: Happy users are more likely to rate
|
|
27
|
+
|
|
28
|
+
### Platform Support
|
|
29
|
+
|
|
30
|
+
- **Android**: Google Play In-App Review API (Android 5.0+)
|
|
31
|
+
- **iOS**: SKStoreReviewController (iOS 10.3+)
|
|
32
|
+
- **Web**: Fallback to custom UI or redirect
|
|
33
|
+
|
|
34
|
+
## Why In-App Reviews Matter
|
|
35
|
+
|
|
36
|
+
### Statistics
|
|
37
|
+
|
|
38
|
+
- Apps with 4+ star ratings see **2x more downloads**
|
|
39
|
+
- **70% of users** look at ratings before downloading
|
|
40
|
+
- In-app review prompts have **4-5x higher engagement** than external links
|
|
41
|
+
|
|
42
|
+
### Impact on ASO (App Store Optimization)
|
|
43
|
+
|
|
44
|
+
- Higher ratings improve search ranking
|
|
45
|
+
- More reviews increase credibility
|
|
46
|
+
- Recent reviews show active development
|
|
47
|
+
|
|
48
|
+
## Platform Guidelines
|
|
49
|
+
|
|
50
|
+
### Apple App Store Guidelines
|
|
51
|
+
|
|
52
|
+
1. **Frequency Limits**:
|
|
53
|
+
- Maximum 3 prompts per 365 days
|
|
54
|
+
- System may show prompt less frequently
|
|
55
|
+
- Cannot check if review was submitted
|
|
56
|
+
|
|
57
|
+
2. **Requirements**:
|
|
58
|
+
- Cannot incentivize reviews
|
|
59
|
+
- Cannot gate features behind reviews
|
|
60
|
+
- Must use official API (no custom UI)
|
|
61
|
+
|
|
62
|
+
### Google Play Guidelines
|
|
63
|
+
|
|
64
|
+
1. **Frequency Limits**:
|
|
65
|
+
- No hard limit, but be reasonable
|
|
66
|
+
- System may throttle excessive requests
|
|
67
|
+
- Cannot check if review was submitted
|
|
68
|
+
|
|
69
|
+
2. **Requirements**:
|
|
70
|
+
- Cannot incentivize reviews
|
|
71
|
+
- Must follow Play Store policies
|
|
72
|
+
- Review flow must complete in-app
|
|
73
|
+
|
|
74
|
+
## Setup Guide
|
|
75
|
+
|
|
76
|
+
### Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install capacitor-native-update
|
|
80
|
+
npx cap sync
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Android Configuration
|
|
84
|
+
|
|
85
|
+
No additional configuration required. The plugin automatically includes the Play Core library.
|
|
86
|
+
|
|
87
|
+
### iOS Configuration
|
|
88
|
+
|
|
89
|
+
No additional configuration required. The plugin automatically uses StoreKit.
|
|
90
|
+
|
|
91
|
+
### Capacitor Configuration
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"plugins": {
|
|
96
|
+
"CapacitorNativeUpdate": {
|
|
97
|
+
"appStoreId": "YOUR_APP_STORE_ID",
|
|
98
|
+
"reviewPromptDelay": 2000,
|
|
99
|
+
"reviewDebugMode": false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Implementation Steps
|
|
106
|
+
|
|
107
|
+
### Step 1: Basic Implementation
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { CapacitorNativeUpdate } from 'capacitor-native-update';
|
|
111
|
+
|
|
112
|
+
export class AppReviewService {
|
|
113
|
+
async requestReview() {
|
|
114
|
+
try {
|
|
115
|
+
const result = await CapacitorNativeUpdate.requestReview();
|
|
116
|
+
|
|
117
|
+
if (result.displayed) {
|
|
118
|
+
console.log('Review prompt was displayed');
|
|
119
|
+
// Track that prompt was shown
|
|
120
|
+
await this.analytics.track('review_prompt_displayed');
|
|
121
|
+
} else {
|
|
122
|
+
console.log('Review prompt was not displayed (system throttled)');
|
|
123
|
+
// Maybe try alternative feedback method
|
|
124
|
+
await this.showAlternativeFeedback();
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Review request failed:', error);
|
|
128
|
+
// Fallback to external review
|
|
129
|
+
await this.openExternalReview();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Step 2: Smart Review Triggers
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
export class SmartReviewManager {
|
|
139
|
+
private readonly REVIEW_CONDITIONS = {
|
|
140
|
+
minSessions: 5,
|
|
141
|
+
minDaysInstalled: 3,
|
|
142
|
+
minActionsCompleted: 10,
|
|
143
|
+
positiveExperienceRequired: true,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
async checkAndRequestReview() {
|
|
147
|
+
// Check if we should ask for review
|
|
148
|
+
const shouldAsk = await this.shouldAskForReview();
|
|
149
|
+
|
|
150
|
+
if (!shouldAsk) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if user had positive experience
|
|
155
|
+
const hasPositiveExperience = await this.checkPositiveExperience();
|
|
156
|
+
|
|
157
|
+
if (!hasPositiveExperience) {
|
|
158
|
+
// Ask for feedback instead
|
|
159
|
+
await this.askForFeedback();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Request review
|
|
164
|
+
await this.requestReview();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async shouldAskForReview(): Promise<boolean> {
|
|
168
|
+
const stats = await this.getUserStats();
|
|
169
|
+
|
|
170
|
+
// Check all conditions
|
|
171
|
+
const conditions = [
|
|
172
|
+
stats.sessionCount >= this.REVIEW_CONDITIONS.minSessions,
|
|
173
|
+
stats.daysSinceInstall >= this.REVIEW_CONDITIONS.minDaysInstalled,
|
|
174
|
+
stats.completedActions >= this.REVIEW_CONDITIONS.minActionsCompleted,
|
|
175
|
+
!stats.hasReviewedBefore,
|
|
176
|
+
!stats.hasDeclinedRecently,
|
|
177
|
+
this.isGoodMoment(),
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
return conditions.every((condition) => condition);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private isGoodMoment(): boolean {
|
|
184
|
+
// Don't interrupt critical flows
|
|
185
|
+
const currentRoute = this.router.url;
|
|
186
|
+
const badMoments = ['/checkout', '/payment', '/onboarding', '/support'];
|
|
187
|
+
|
|
188
|
+
return !badMoments.some((route) => currentRoute.includes(route));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async checkPositiveExperience(): Promise<boolean> {
|
|
192
|
+
// Check recent user actions
|
|
193
|
+
const recentActions = await this.getRecentUserActions();
|
|
194
|
+
|
|
195
|
+
const positiveSignals = [
|
|
196
|
+
recentActions.includes('task_completed'),
|
|
197
|
+
recentActions.includes('content_shared'),
|
|
198
|
+
recentActions.includes('milestone_achieved'),
|
|
199
|
+
!recentActions.includes('error_encountered'),
|
|
200
|
+
!recentActions.includes('support_contacted'),
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Need at least 3 positive signals
|
|
204
|
+
return positiveSignals.filter((signal) => signal).length >= 3;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Step 3: Review Trigger Points
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
export class ReviewTriggerPoints {
|
|
213
|
+
constructor(
|
|
214
|
+
private reviewService: SmartReviewManager,
|
|
215
|
+
private analytics: AnalyticsService
|
|
216
|
+
) {}
|
|
217
|
+
|
|
218
|
+
// After completing important action
|
|
219
|
+
async onTaskCompleted() {
|
|
220
|
+
await this.incrementPositiveAction('task_completed');
|
|
221
|
+
|
|
222
|
+
// Check if this is a milestone
|
|
223
|
+
const taskCount = await this.getCompletedTaskCount();
|
|
224
|
+
if (taskCount % 5 === 0) {
|
|
225
|
+
// Every 5 tasks
|
|
226
|
+
await this.reviewService.checkAndRequestReview();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// After successful transaction
|
|
231
|
+
async onPurchaseCompleted() {
|
|
232
|
+
await this.incrementPositiveAction('purchase_completed');
|
|
233
|
+
|
|
234
|
+
// Wait a bit before asking
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
this.reviewService.checkAndRequestReview();
|
|
237
|
+
}, 5000);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// After achieving milestone
|
|
241
|
+
async onMilestoneAchieved(milestone: string) {
|
|
242
|
+
await this.incrementPositiveAction('milestone_achieved');
|
|
243
|
+
|
|
244
|
+
// Show achievement first
|
|
245
|
+
await this.showAchievementToast(milestone);
|
|
246
|
+
|
|
247
|
+
// Then ask for review
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
this.reviewService.checkAndRequestReview();
|
|
250
|
+
}, 3000);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// After positive feedback
|
|
254
|
+
async onPositiveFeedback() {
|
|
255
|
+
// User already indicated satisfaction
|
|
256
|
+
await this.reviewService.requestReview();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// App foregrounded (for time-based triggers)
|
|
260
|
+
async onAppForegrounded() {
|
|
261
|
+
const lastPrompt = await this.getLastReviewPromptDate();
|
|
262
|
+
const daysSinceLastPrompt = this.daysSince(lastPrompt);
|
|
263
|
+
|
|
264
|
+
// Check every 30 days
|
|
265
|
+
if (daysSinceLastPrompt >= 30) {
|
|
266
|
+
await this.reviewService.checkAndRequestReview();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Step 4: Two-Step Review Flow
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
export class TwoStepReviewFlow {
|
|
276
|
+
async initiateReviewFlow() {
|
|
277
|
+
// Step 1: Ask if they enjoy the app
|
|
278
|
+
const enjoys = await this.askEnjoyment();
|
|
279
|
+
|
|
280
|
+
if (enjoys) {
|
|
281
|
+
// Step 2: Ask for review
|
|
282
|
+
await this.askForReview();
|
|
283
|
+
} else {
|
|
284
|
+
// Ask for feedback instead
|
|
285
|
+
await this.askForFeedback();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private async askEnjoyment(): Promise<boolean> {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const alert = this.alertController.create({
|
|
292
|
+
header: 'Enjoying the app?',
|
|
293
|
+
message: 'How has your experience been so far?',
|
|
294
|
+
buttons: [
|
|
295
|
+
{
|
|
296
|
+
text: 'Not really',
|
|
297
|
+
handler: () => {
|
|
298
|
+
this.analytics.track('review_enjoyment_negative');
|
|
299
|
+
resolve(false);
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
text: 'Yes!',
|
|
304
|
+
handler: () => {
|
|
305
|
+
this.analytics.track('review_enjoyment_positive');
|
|
306
|
+
resolve(true);
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
alert.present();
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async askForReview() {
|
|
316
|
+
const alert = await this.alertController.create({
|
|
317
|
+
header: 'Awesome!',
|
|
318
|
+
message: 'Would you mind rating us on the app store?',
|
|
319
|
+
buttons: [
|
|
320
|
+
{
|
|
321
|
+
text: 'No thanks',
|
|
322
|
+
role: 'cancel',
|
|
323
|
+
handler: () => {
|
|
324
|
+
this.analytics.track('review_declined');
|
|
325
|
+
this.markDeclined();
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
text: 'Sure!',
|
|
330
|
+
handler: async () => {
|
|
331
|
+
this.analytics.track('review_accepted');
|
|
332
|
+
await CapacitorNativeUpdate.requestReview();
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
await alert.present();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async askForFeedback() {
|
|
341
|
+
const alert = await this.alertController.create({
|
|
342
|
+
header: 'We appreciate your feedback',
|
|
343
|
+
message: 'What can we do to improve your experience?',
|
|
344
|
+
inputs: [
|
|
345
|
+
{
|
|
346
|
+
name: 'feedback',
|
|
347
|
+
type: 'textarea',
|
|
348
|
+
placeholder: 'Your feedback...',
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
buttons: [
|
|
352
|
+
{
|
|
353
|
+
text: 'Cancel',
|
|
354
|
+
role: 'cancel',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
text: 'Send',
|
|
358
|
+
handler: (data) => {
|
|
359
|
+
this.submitFeedback(data.feedback);
|
|
360
|
+
this.showThankYou();
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
await alert.present();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Step 5: Alternative Review Methods
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
export class AlternativeReviewMethods {
|
|
374
|
+
// For when in-app review is not available
|
|
375
|
+
async openExternalReview() {
|
|
376
|
+
const platform = Capacitor.getPlatform();
|
|
377
|
+
|
|
378
|
+
if (platform === 'ios') {
|
|
379
|
+
await this.openAppStore();
|
|
380
|
+
} else if (platform === 'android') {
|
|
381
|
+
await this.openPlayStore();
|
|
382
|
+
} else {
|
|
383
|
+
await this.openWebReview();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private async openAppStore() {
|
|
388
|
+
const appStoreId = 'YOUR_APP_STORE_ID';
|
|
389
|
+
const reviewUrl = `https://apps.apple.com/app/id${appStoreId}?action=write-review`;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await Browser.open({ url: reviewUrl });
|
|
393
|
+
this.analytics.track('external_review_opened', { platform: 'ios' });
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('Failed to open App Store:', error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async openPlayStore() {
|
|
400
|
+
const packageName = 'com.example.app';
|
|
401
|
+
const playStoreUrl = `https://play.google.com/store/apps/details?id=${packageName}`;
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await Browser.open({ url: playStoreUrl });
|
|
405
|
+
this.analytics.track('external_review_opened', { platform: 'android' });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error('Failed to open Play Store:', error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private async openWebReview() {
|
|
412
|
+
// Custom web review form or third-party service
|
|
413
|
+
const modal = await this.modalController.create({
|
|
414
|
+
component: WebReviewComponent,
|
|
415
|
+
});
|
|
416
|
+
await modal.present();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Best Practices
|
|
422
|
+
|
|
423
|
+
### 1. Timing is Everything
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
export class ReviewTimingStrategy {
|
|
427
|
+
// Good moments to ask
|
|
428
|
+
private readonly GOOD_MOMENTS = [
|
|
429
|
+
'after_milestone_achieved',
|
|
430
|
+
'after_successful_transaction',
|
|
431
|
+
'after_positive_interaction',
|
|
432
|
+
'after_content_shared',
|
|
433
|
+
'after_returning_user_session',
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
// Bad moments to avoid
|
|
437
|
+
private readonly BAD_MOMENTS = [
|
|
438
|
+
'during_onboarding',
|
|
439
|
+
'after_error',
|
|
440
|
+
'during_payment_flow',
|
|
441
|
+
'immediately_after_install',
|
|
442
|
+
'after_support_contact',
|
|
443
|
+
'during_critical_task',
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
async isGoodMomentForReview(): Promise<boolean> {
|
|
447
|
+
const currentContext = await this.getCurrentUserContext();
|
|
448
|
+
|
|
449
|
+
// Check if it's a bad moment
|
|
450
|
+
if (this.BAD_MOMENTS.includes(currentContext.state)) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check if it's explicitly a good moment
|
|
455
|
+
if (this.GOOD_MOMENTS.includes(currentContext.state)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Additional checks
|
|
460
|
+
return this.additionalTimingChecks(currentContext);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private async additionalTimingChecks(context: any): Promise<boolean> {
|
|
464
|
+
// Don't ask too soon after install
|
|
465
|
+
if (context.daysSinceInstall < 3) return false;
|
|
466
|
+
|
|
467
|
+
// Don't ask if user is in a hurry
|
|
468
|
+
if (context.sessionDuration < 60) return false;
|
|
469
|
+
|
|
470
|
+
// Don't ask if user had recent issues
|
|
471
|
+
if (context.recentErrors > 0) return false;
|
|
472
|
+
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 2. Respect User Choice
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
export class ReviewPreferenceManager {
|
|
482
|
+
private readonly PREFERENCE_KEY = 'review_preferences';
|
|
483
|
+
|
|
484
|
+
async userDeclinedReview() {
|
|
485
|
+
const prefs = await this.getPreferences();
|
|
486
|
+
prefs.declineCount++;
|
|
487
|
+
prefs.lastDeclineDate = new Date().toISOString();
|
|
488
|
+
|
|
489
|
+
// After 3 declines, stop asking
|
|
490
|
+
if (prefs.declineCount >= 3) {
|
|
491
|
+
prefs.permanentlyDeclined = true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await this.savePreferences(prefs);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async canAskForReview(): Promise<boolean> {
|
|
498
|
+
const prefs = await this.getPreferences();
|
|
499
|
+
|
|
500
|
+
// Never ask if permanently declined
|
|
501
|
+
if (prefs.permanentlyDeclined) return false;
|
|
502
|
+
|
|
503
|
+
// Wait longer after each decline
|
|
504
|
+
const daysSinceDecline = this.daysSince(prefs.lastDeclineDate);
|
|
505
|
+
const requiredDays = prefs.declineCount * 30; // 30, 60, 90 days
|
|
506
|
+
|
|
507
|
+
return daysSinceDecline >= requiredDays;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async resetAfterPositiveReview() {
|
|
511
|
+
const prefs = await this.getPreferences();
|
|
512
|
+
prefs.hasReviewed = true;
|
|
513
|
+
prefs.reviewDate = new Date().toISOString();
|
|
514
|
+
prefs.declineCount = 0;
|
|
515
|
+
await this.savePreferences(prefs);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### 3. A/B Testing
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
export class ReviewABTesting {
|
|
524
|
+
async getReviewStrategy(): Promise<string> {
|
|
525
|
+
const userId = await this.getUserId();
|
|
526
|
+
const variant = this.hashUserId(userId) % 3;
|
|
527
|
+
|
|
528
|
+
switch (variant) {
|
|
529
|
+
case 0:
|
|
530
|
+
return 'immediate'; // Direct review request
|
|
531
|
+
case 1:
|
|
532
|
+
return 'two-step'; // Ask enjoyment first
|
|
533
|
+
case 2:
|
|
534
|
+
return 'contextual'; // Wait for positive moment
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async executeStrategy(strategy: string) {
|
|
539
|
+
this.analytics.track('review_strategy_assigned', { strategy });
|
|
540
|
+
|
|
541
|
+
switch (strategy) {
|
|
542
|
+
case 'immediate':
|
|
543
|
+
await CapacitorNativeUpdate.requestReview();
|
|
544
|
+
break;
|
|
545
|
+
case 'two-step':
|
|
546
|
+
await this.twoStepFlow.initiateReviewFlow();
|
|
547
|
+
break;
|
|
548
|
+
case 'contextual':
|
|
549
|
+
await this.contextualFlow.waitForPositiveMoment();
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Analytics Integration
|
|
557
|
+
|
|
558
|
+
### Track Everything
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
export class ReviewAnalytics {
|
|
562
|
+
private events = {
|
|
563
|
+
// Prompt events
|
|
564
|
+
PROMPT_TRIGGERED: 'review_prompt_triggered',
|
|
565
|
+
PROMPT_DISPLAYED: 'review_prompt_displayed',
|
|
566
|
+
PROMPT_DISMISSED: 'review_prompt_dismissed',
|
|
567
|
+
|
|
568
|
+
// User actions
|
|
569
|
+
REVIEW_ACCEPTED: 'review_accepted',
|
|
570
|
+
REVIEW_DECLINED: 'review_declined',
|
|
571
|
+
REVIEW_COMPLETED: 'review_completed', // Best guess
|
|
572
|
+
|
|
573
|
+
// Feedback events
|
|
574
|
+
FEEDBACK_PROVIDED: 'feedback_provided',
|
|
575
|
+
EXTERNAL_REVIEW: 'external_review_opened',
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
async trackReviewFlow(stage: string, properties?: any) {
|
|
579
|
+
await this.analytics.track(this.events[stage], {
|
|
580
|
+
...properties,
|
|
581
|
+
timestamp: new Date().toISOString(),
|
|
582
|
+
sessionId: this.sessionId,
|
|
583
|
+
userId: this.userId,
|
|
584
|
+
platform: Capacitor.getPlatform(),
|
|
585
|
+
appVersion: this.appVersion,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async generateReviewReport(): Promise<ReviewMetrics> {
|
|
590
|
+
const metrics = await this.analytics.query({
|
|
591
|
+
events: Object.values(this.events),
|
|
592
|
+
timeframe: 'last_30_days',
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
promptsShown: metrics[this.events.PROMPT_DISPLAYED],
|
|
597
|
+
acceptanceRate:
|
|
598
|
+
metrics[this.events.REVIEW_ACCEPTED] /
|
|
599
|
+
metrics[this.events.PROMPT_DISPLAYED],
|
|
600
|
+
declineRate:
|
|
601
|
+
metrics[this.events.REVIEW_DECLINED] /
|
|
602
|
+
metrics[this.events.PROMPT_DISPLAYED],
|
|
603
|
+
estimatedCompletionRate: this.estimateCompletionRate(metrics),
|
|
604
|
+
feedbackRate:
|
|
605
|
+
metrics[this.events.FEEDBACK_PROVIDED] /
|
|
606
|
+
metrics[this.events.PROMPT_DISPLAYED],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Testing
|
|
613
|
+
|
|
614
|
+
### Development Testing
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
export class ReviewTestingUtils {
|
|
618
|
+
async enableTestMode() {
|
|
619
|
+
// Enable debug mode
|
|
620
|
+
await CapacitorNativeUpdate.setReviewDebugMode({ enabled: true });
|
|
621
|
+
|
|
622
|
+
// Reset all preferences
|
|
623
|
+
await this.clearReviewPreferences();
|
|
624
|
+
|
|
625
|
+
// Set up test conditions
|
|
626
|
+
await this.setupTestConditions();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async simulateReviewFlow() {
|
|
630
|
+
console.log('Simulating review flow...');
|
|
631
|
+
|
|
632
|
+
// Force display of review prompt
|
|
633
|
+
const result = await CapacitorNativeUpdate.requestReview({
|
|
634
|
+
force: true, // Only works in debug mode
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
console.log('Review simulation result:', result);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async testDifferentScenarios() {
|
|
641
|
+
const scenarios = [
|
|
642
|
+
{ name: 'First time user', daysSinceInstall: 0 },
|
|
643
|
+
{ name: 'Happy user', positiveActions: 10 },
|
|
644
|
+
{ name: 'Frustrated user', errors: 5 },
|
|
645
|
+
{ name: 'Returning user', sessions: 20 },
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
for (const scenario of scenarios) {
|
|
649
|
+
await this.setupScenario(scenario);
|
|
650
|
+
const shouldShow = await this.reviewManager.shouldAskForReview();
|
|
651
|
+
console.log(
|
|
652
|
+
`Scenario "${scenario.name}": ${shouldShow ? 'SHOW' : 'HIDE'}`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Platform-Specific Testing
|
|
660
|
+
|
|
661
|
+
#### iOS Testing
|
|
662
|
+
|
|
663
|
+
1. Use development build (not TestFlight)
|
|
664
|
+
2. Reviews work in debug mode
|
|
665
|
+
3. Can test multiple times
|
|
666
|
+
4. Check console for SKStoreReviewController logs
|
|
667
|
+
|
|
668
|
+
#### Android Testing
|
|
669
|
+
|
|
670
|
+
1. Use internal test track
|
|
671
|
+
2. Sign in with test account
|
|
672
|
+
3. Clear Play Store cache between tests
|
|
673
|
+
4. Check Play Console for metrics
|
|
674
|
+
|
|
675
|
+
## Troubleshooting
|
|
676
|
+
|
|
677
|
+
### Common Issues
|
|
678
|
+
|
|
679
|
+
#### 1. Review Prompt Not Showing
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// Debug checklist
|
|
683
|
+
async function debugReviewPrompt() {
|
|
684
|
+
// Check if available on platform
|
|
685
|
+
const isAvailable = await CapacitorNativeUpdate.isReviewAvailable();
|
|
686
|
+
console.log('Review available:', isAvailable);
|
|
687
|
+
|
|
688
|
+
// Check system throttling
|
|
689
|
+
const debugInfo = await CapacitorNativeUpdate.getReviewDebugInfo();
|
|
690
|
+
console.log('Debug info:', debugInfo);
|
|
691
|
+
|
|
692
|
+
// Check your conditions
|
|
693
|
+
const conditions = await this.checkAllConditions();
|
|
694
|
+
console.log('App conditions:', conditions);
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
#### 2. Low Review Rates
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// Optimize review strategy
|
|
702
|
+
class ReviewOptimizer {
|
|
703
|
+
async analyzeAndOptimize() {
|
|
704
|
+
const metrics = await this.getReviewMetrics();
|
|
705
|
+
|
|
706
|
+
if (metrics.promptsShown < 100) {
|
|
707
|
+
console.log('Not enough data yet');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (metrics.acceptanceRate < 0.1) {
|
|
712
|
+
// Less than 10% accepting
|
|
713
|
+
console.log('Consider:');
|
|
714
|
+
console.log('- Improving timing');
|
|
715
|
+
console.log('- Using two-step flow');
|
|
716
|
+
console.log('- Checking for negative experiences');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (metrics.promptsShown / metrics.eligibleUsers < 0.5) {
|
|
720
|
+
console.log('Not reaching enough users');
|
|
721
|
+
console.log('- Relax conditions');
|
|
722
|
+
console.log('- Add more trigger points');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
#### 3. Platform-Specific Issues
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
// Platform fallbacks
|
|
732
|
+
async function requestReviewWithFallback() {
|
|
733
|
+
try {
|
|
734
|
+
const result = await CapacitorNativeUpdate.requestReview();
|
|
735
|
+
|
|
736
|
+
if (!result.displayed) {
|
|
737
|
+
// System didn't show prompt
|
|
738
|
+
if (Capacitor.getPlatform() === 'web') {
|
|
739
|
+
await this.showWebReviewUI();
|
|
740
|
+
} else {
|
|
741
|
+
// Try again later
|
|
742
|
+
await this.scheduleRetry();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch (error) {
|
|
746
|
+
// API not available
|
|
747
|
+
await this.openExternalReview();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
## Summary
|
|
753
|
+
|
|
754
|
+
Key takeaways for implementing app reviews:
|
|
755
|
+
|
|
756
|
+
1. **Time it right** - Ask when users are happy
|
|
757
|
+
2. **Be respectful** - Don't ask too often
|
|
758
|
+
3. **Track everything** - Measure and optimize
|
|
759
|
+
4. **Handle all cases** - Have fallbacks ready
|
|
760
|
+
5. **Follow guidelines** - Respect platform rules
|
|
761
|
+
6. **Test thoroughly** - Use debug modes
|
|
762
|
+
7. **Iterate** - Continuously improve based on data
|
|
763
|
+
|
|
764
|
+
## Next Steps
|
|
765
|
+
|
|
766
|
+
- Review the [Quick Start Guide](./QUICK_START.md)
|
|
767
|
+
- Check the [API Reference](../API.md)
|
|
768
|
+
- See [Example Implementation](../example/src/services/review.service.ts)
|