App Store Review
Prompt users to rate your app using the native app store review dialog on iOS and Android.
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.
Repository: github.com/PerryTS/appstorereview
Quick start
1. Add the extension
Clone or copy the extension into your project’s extensions directory:
mkdir -p extensions
cd extensions
git clone https://github.com/PerryTS/appstorereview.git perry-appstore-review
cd ..
Your project structure:
my-app/
├── package.json
├── src/
│ └── index.ts
└── extensions/
└── perry-appstore-review/
2. Use in your app
import { requestReview } from "perry-appstore-review";
// Show the review prompt when the user completes a meaningful action
async function onLevelComplete() {
await requestReview();
}
3. Build
perry src/index.ts -o app --target ios --bundle-extensions ./extensions
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.
API
requestReview(): Promise<void>
Opens the native app store review prompt. Returns a promise that resolves when the prompt has been presented (or skipped by the OS).
import { requestReview } from "perry-appstore-review";
await requestReview();
The function only triggers the prompt. It does not:
- Track whether the user has already reviewed
- Throttle how often the prompt appears (iOS does this automatically; Android does not)
- Return whether the user actually left a review (neither platform provides this)
Platform behavior
iOS
Uses SKStoreReviewController.requestReview(in:) from StoreKit.
| Detail | Value |
|---|---|
| Native API | SKStoreReviewController.requestReview(in: UIWindowScene) |
| Minimum iOS version | 14.0 |
| Framework | StoreKit |
| Thread | Dispatched to main thread automatically |
| Throttling | Apple limits display to 3 times per 365-day period per app. The system may silently ignore the call. |
| Development builds | Always shown in debug/TestFlight builds |
| User control | Users can disable review prompts in Settings > App Store |
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.
macOS
Uses the same StoreKit API. Shares the iOS native crate (both compile from crate-ios).
| Detail | Value |
|---|---|
| Native API | SKStoreReviewController.requestReview() |
| Minimum macOS version | 13.0 |
| Framework | StoreKit |
| Throttling | Same as iOS — system-controlled |
Only works for apps distributed through the Mac App Store.
Android
Uses the Google Play In-App Review API.
| Detail | Value |
|---|---|
| Native API | ReviewManager.requestReviewFlow() + launchReviewFlow() |
| Library | com.google.android.play:review |
| Minimum API level | 21 (Android 5.0) |
| Throttling | Google enforces a quota — the prompt may not appear every time |
| Execution | Runs on a background thread to avoid blocking the UI |
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:
dependencies {
implementation 'com.google.android.play:review:2.0.2'
}
Without this dependency, requestReview() will resolve with an error explaining the missing library.
Other platforms
On unsupported platforms (Linux, Windows, Web), requestReview() resolves immediately with an error. It will not throw — your app continues normally.
Best practices
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.
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:
import { requestReview } from "perry-appstore-review";
import { preferencesGet, preferencesSet } from "perry/system";
async function maybeAskForReview() {
const lastAsked = Number(preferencesGet("lastReviewAsk") || "0");
const now = Date.now();
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
if (now - lastAsked > thirtyDays) {
preferencesSet("lastReviewAsk", String(now));
await requestReview();
}
}
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.
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.
Extension structure
The extension follows the standard native extension layout:
perry-appstore-review/
├── package.json # Declares sb_appreview_request function
├── src/
│ └── index.ts # Exports requestReview()
├── crate-ios/ # iOS/macOS: Swift → SKStoreReviewController
│ ├── Cargo.toml
│ ├── build.rs # Compiles Swift to static library
│ ├── src/lib.rs # Rust FFI bridge
│ └── swift/review_bridge.swift
├── crate-android/ # Android: JNI → Play In-App Review API
│ ├── Cargo.toml
│ └── src/lib.rs
└── crate-stub/ # Other platforms: resolves with error
├── Cargo.toml
└── src/lib.rs
One native function is declared in package.json:
{
"perry": {
"nativeLibrary": {
"functions": [
{ "name": "sb_appreview_request", "params": [], "returns": "f64" }
]
}
}
}
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.
Next Steps
- Native Extensions — How native extensions work, creating your own
- iOS Platform — iOS platform guide
- Android Platform — Android platform guide