|
| 1 | +# App Store Review |
| 2 | + |
| 3 | +Prompt users to rate your app using the native app store review dialog on iOS and Android. |
| 4 | + |
| 5 | +The `perry-appstore-review` extension exposes a single function — `requestReview()` — that opens the platform's native review prompt. It does nothing else: when and how often to ask is entirely up to you. |
| 6 | + |
| 7 | +**Repository:** [github.com/PerryTS/appstorereview](https://github.com/PerryTS/appstorereview) |
| 8 | + |
| 9 | +## Quick start |
| 10 | + |
| 11 | +### 1. Add the extension |
| 12 | + |
| 13 | +Clone or copy the extension into your project's extensions directory: |
| 14 | + |
| 15 | +```bash |
| 16 | +mkdir -p extensions |
| 17 | +cd extensions |
| 18 | +git clone https://github.com/PerryTS/appstorereview.git perry-appstore-review |
| 19 | +cd .. |
| 20 | +``` |
| 21 | + |
| 22 | +Your project structure: |
| 23 | + |
| 24 | +``` |
| 25 | +my-app/ |
| 26 | +├── package.json |
| 27 | +├── src/ |
| 28 | +│ └── index.ts |
| 29 | +└── extensions/ |
| 30 | + └── perry-appstore-review/ |
| 31 | +``` |
| 32 | + |
| 33 | +### 2. Use in your app |
| 34 | + |
| 35 | +```typescript |
| 36 | +import { requestReview } from "perry-appstore-review"; |
| 37 | + |
| 38 | +// Show the review prompt when the user completes a meaningful action |
| 39 | +async function onLevelComplete() { |
| 40 | + await requestReview(); |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +### 3. Build |
| 45 | + |
| 46 | +```bash |
| 47 | +perry src/index.ts -o app --target ios --bundle-extensions ./extensions |
| 48 | +``` |
| 49 | + |
| 50 | +The `--bundle-extensions` flag tells Perry to discover, compile, and link all native extensions in the given directory. The app store review native code is compiled and statically linked into your binary — no runtime dependencies. |
| 51 | + |
| 52 | +## API |
| 53 | + |
| 54 | +### `requestReview(): Promise<void>` |
| 55 | + |
| 56 | +Opens the native app store review prompt. Returns a promise that resolves when the prompt has been presented (or skipped by the OS). |
| 57 | + |
| 58 | +```typescript |
| 59 | +import { requestReview } from "perry-appstore-review"; |
| 60 | + |
| 61 | +await requestReview(); |
| 62 | +``` |
| 63 | + |
| 64 | +The function only triggers the prompt. It does not: |
| 65 | +- Track whether the user has already reviewed |
| 66 | +- Throttle how often the prompt appears (iOS does this automatically; Android does not) |
| 67 | +- Return whether the user actually left a review (neither platform provides this) |
| 68 | + |
| 69 | +## Platform behavior |
| 70 | + |
| 71 | +### iOS |
| 72 | + |
| 73 | +Uses [`SKStoreReviewController.requestReview(in:)`](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/requestreview(in:)) from StoreKit. |
| 74 | + |
| 75 | +| Detail | Value | |
| 76 | +|--------|-------| |
| 77 | +| Native API | `SKStoreReviewController.requestReview(in: UIWindowScene)` | |
| 78 | +| Minimum iOS version | 14.0 | |
| 79 | +| Framework | StoreKit | |
| 80 | +| Thread | Dispatched to main thread automatically | |
| 81 | +| Throttling | Apple limits display to 3 times per 365-day period per app. The system may silently ignore the call. | |
| 82 | +| Development builds | Always shown in debug/TestFlight builds | |
| 83 | +| User control | Users can disable review prompts in Settings > App Store | |
| 84 | + |
| 85 | +**Important:** Apple's throttling means the prompt is not guaranteed to appear every time `requestReview()` is called. Design your app flow so that not showing the prompt doesn't break the user experience. |
| 86 | + |
| 87 | +### macOS |
| 88 | + |
| 89 | +Uses the same StoreKit API. Shares the iOS native crate (both compile from `crate-ios`). |
| 90 | + |
| 91 | +| Detail | Value | |
| 92 | +|--------|-------| |
| 93 | +| Native API | `SKStoreReviewController.requestReview()` | |
| 94 | +| Minimum macOS version | 13.0 | |
| 95 | +| Framework | StoreKit | |
| 96 | +| Throttling | Same as iOS — system-controlled | |
| 97 | + |
| 98 | +Only works for apps distributed through the Mac App Store. |
| 99 | + |
| 100 | +### Android |
| 101 | + |
| 102 | +Uses the [Google Play In-App Review API](https://developer.android.com/guide/playcore/in-app-review). |
| 103 | + |
| 104 | +| Detail | Value | |
| 105 | +|--------|-------| |
| 106 | +| Native API | `ReviewManager.requestReviewFlow()` + `launchReviewFlow()` | |
| 107 | +| Library | `com.google.android.play:review` | |
| 108 | +| Minimum API level | 21 (Android 5.0) | |
| 109 | +| Throttling | Google enforces a quota — the prompt may not appear every time | |
| 110 | +| Execution | Runs on a background thread to avoid blocking the UI | |
| 111 | + |
| 112 | +**Required Gradle dependency:** The Google Play In-App Review API is not part of the Android SDK. You must add it to your app's `build.gradle`: |
| 113 | + |
| 114 | +```groovy |
| 115 | +dependencies { |
| 116 | + implementation 'com.google.android.play:review:2.0.2' |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +Without this dependency, `requestReview()` will resolve with an error explaining the missing library. |
| 121 | + |
| 122 | +### Other platforms |
| 123 | + |
| 124 | +On unsupported platforms (Linux, Windows, Web), `requestReview()` resolves immediately with an error. It will not throw — your app continues normally. |
| 125 | + |
| 126 | +## Best practices |
| 127 | + |
| 128 | +**Do ask at the right moment.** Prompt after a positive experience — completing a level, finishing a task, achieving a goal. Don't ask on first launch or during onboarding. |
| 129 | + |
| 130 | +**Don't ask too often.** Even though iOS throttles automatically, Android does not have the same strict limits. Implement your own logic to track when you last asked: |
| 131 | + |
| 132 | +```typescript |
| 133 | +import { requestReview } from "perry-appstore-review"; |
| 134 | +import { preferencesGet, preferencesSet } from "perry/system"; |
| 135 | + |
| 136 | +async function maybeAskForReview() { |
| 137 | + const lastAsked = Number(preferencesGet("lastReviewAsk") || "0"); |
| 138 | + const now = Date.now(); |
| 139 | + const thirtyDays = 30 * 24 * 60 * 60 * 1000; |
| 140 | + |
| 141 | + if (now - lastAsked > thirtyDays) { |
| 142 | + preferencesSet("lastReviewAsk", String(now)); |
| 143 | + await requestReview(); |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +**Don't condition app behavior on the review.** Neither iOS nor Android tells you whether the user left a review, gave a rating, or dismissed the prompt. The promise resolving does not mean a review was submitted. |
| 149 | + |
| 150 | +**Don't use custom review dialogs before the native one.** Both Apple and Google discourage showing your own "Rate this app?" dialog before the native prompt. The native prompt is designed to be low-friction — adding a pre-prompt increases abandonment. |
| 151 | + |
| 152 | +## Extension structure |
| 153 | + |
| 154 | +The extension follows the standard [native extension](native-extensions.md) layout: |
| 155 | + |
| 156 | +``` |
| 157 | +perry-appstore-review/ |
| 158 | +├── package.json # Declares sb_appreview_request function |
| 159 | +├── src/ |
| 160 | +│ └── index.ts # Exports requestReview() |
| 161 | +├── crate-ios/ # iOS/macOS: Swift → SKStoreReviewController |
| 162 | +│ ├── Cargo.toml |
| 163 | +│ ├── build.rs # Compiles Swift to static library |
| 164 | +│ ├── src/lib.rs # Rust FFI bridge |
| 165 | +│ └── swift/review_bridge.swift |
| 166 | +├── crate-android/ # Android: JNI → Play In-App Review API |
| 167 | +│ ├── Cargo.toml |
| 168 | +│ └── src/lib.rs |
| 169 | +└── crate-stub/ # Other platforms: resolves with error |
| 170 | + ├── Cargo.toml |
| 171 | + └── src/lib.rs |
| 172 | +``` |
| 173 | + |
| 174 | +One native function is declared in `package.json`: |
| 175 | + |
| 176 | +```json |
| 177 | +{ |
| 178 | + "perry": { |
| 179 | + "nativeLibrary": { |
| 180 | + "functions": [ |
| 181 | + { "name": "sb_appreview_request", "params": [], "returns": "f64" } |
| 182 | + ] |
| 183 | + } |
| 184 | + } |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +The TypeScript layer wraps this into the public `requestReview()` function. The native layer creates a Perry promise, calls the platform API, and resolves the promise when done. |
| 189 | + |
| 190 | +## Next Steps |
| 191 | + |
| 192 | +- [Native Extensions](native-extensions.md) — How native extensions work, creating your own |
| 193 | +- [iOS Platform](../platforms/ios.md) — iOS platform guide |
| 194 | +- [Android Platform](../platforms/android.md) — Android platform guide |
0 commit comments