Skip to content

Commit 8577658

Browse files
committed
docs: add native extensions and app store review documentation
Add two new pages to the plugins section: - Native Extensions: how to use and create extensions with platform-native code (perry.nativeLibrary manifest, Rust/Swift/JNI patterns, build flow) - App Store Review: usage guide for the perry-appstore-review extension (iOS SKStoreReviewController, Android Play In-App Review API)
1 parent 7623dec commit 8577658

4 files changed

Lines changed: 503 additions & 0 deletions

File tree

docs/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@
9696
- [Overview](plugins/overview.md)
9797
- [Creating Plugins](plugins/creating-plugins.md)
9898
- [Hooks & Events](plugins/hooks-and-events.md)
99+
- [Native Extensions](plugins/native-extensions.md)
100+
- [App Store Review](plugins/appstore-review.md)
99101

100102
# Testing
101103

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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

Comments
 (0)