Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

DetailValue
Native APISKStoreReviewController.requestReview(in: UIWindowScene)
Minimum iOS version14.0
FrameworkStoreKit
ThreadDispatched to main thread automatically
ThrottlingApple limits display to 3 times per 365-day period per app. The system may silently ignore the call.
Development buildsAlways shown in debug/TestFlight builds
User controlUsers 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).

DetailValue
Native APISKStoreReviewController.requestReview()
Minimum macOS version13.0
FrameworkStoreKit
ThrottlingSame as iOS — system-controlled

Only works for apps distributed through the Mac App Store.

Android

Uses the Google Play In-App Review API.

DetailValue
Native APIReviewManager.requestReviewFlow() + launchReviewFlow()
Librarycom.google.android.play:review
Minimum API level21 (Android 5.0)
ThrottlingGoogle enforces a quota — the prompt may not appear every time
ExecutionRuns 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