星の数が二週間動かなかったことがあります。
expo-store-review は入れてありました。ログには requestReview() を呼んだ形跡が毎日何十件も残っている。なのに App Store Connect の評価件数は前の週とほぼ同じ。呼んでいるのに増えない、という状態がいちばん厄介です。エラーも出ないので、どこを直せばいいのか手掛かりがないのです。
原因は単純でした。OS はレビューダイアログを呼び出しても表示するとは限らず、しかも「表示したかどうか」を教えてくれません。私はずっと、届いていない依頼を数えて満足していたわけです。
このメモは、Rork(React Native / Expo)で作ったアプリの「レビュー依頼が黙って握りつぶされている」状態を、表示機会という自前の指標で可視化して立て直すまでの記録です。個人開発で運用しているアプリで実際に星の増分が動いた手順に絞って書きます。
呼び出しは成功、でも表示はされていない
まず前提を正確に押さえます。iOS の SKStoreReviewController.requestReview()(Expo では StoreReview.requestReview())には、次の制約があります。
| 制約 | 内容 | 開発者から見えるか |
|---|---|---|
| 年間表示上限 | 同一ユーザーに対して概ね年3回まで | 見えない |
| 表示の最終判断 | 呼び出しても OS が出さないことがある | 見えない |
| コールバック | 表示されたか・投稿されたかの戻り値なし | 取得不可 |
| 開発ビルド | シミュレータでは常に表示(本番挙動と異なる) | 誤解の元 |
Android の Google Play In-App Review API も同じ思想です。1日あたりのクォータがあり、短時間に繰り返すと以降は何も起きません。やはり「表示された」という確証は返りません。
つまり requestReview() の呼び出し回数は、レビュー体験の指標として役に立たないのです。呼べば呼ぶほど内部クォータを消費し、肝心なタイミングで出せなくなる。私が二週間はまっていたのは、まさにこの浪費でした。
呼び出しではなく「表示機会」を数える
戻り値がない以上、表示回数そのものは取れません。取れないものを追うのはやめて、代わりに自分が制御できる一歩手前の指標を置きます。「依頼を出してよい条件を満たしてダイアログ表示をリクエストした瞬間(=表示機会)」を、自前のカウンタで記録するのです。
考え方はこうです。OS のクォータは触れない。ならばアプリ側のゲート(いつ・誰に・何回まで依頼するか)を厳密に管理し、そこを通過した回数だけを信頼できる指標として扱います。表示機会を絞り込めば、限られた OS クォータを価値の高い瞬間に集中投下できます。
// src/lib/reviewGate.ts
import * as StoreReview from 'expo-store-review';
import AsyncStorage from '@react-native-async-storage/async-storage';
const KEYS = {
opportunities: 'review_opportunities', // 表示機会に到達した回数
lastPromptAt: 'review_last_prompt_at', // 最後に依頼した日時(ISO)
positiveSignals: 'review_positive_signals', // 良い体験の蓄積
version: 'review_prompted_version', // 依頼済みのアプリバージョン
};
// この2つは「無駄打ちを防ぐ」ための自前ルール。OSの上限とは別に効かせる
const MIN_DAYS_BETWEEN = 60; // 前回依頼からの最低間隔(日)
const REQUIRED_SIGNALS = 3; // 依頼前に必要な良い体験の数
async function getNumber(key: string): Promise<number> {
const raw = await AsyncStorage.getItem(key);
return raw ? Number(raw) : 0;
}
// 良い体験(タスク完了・保存成功など)のたびに加点する
export async function recordPositiveSignal(): Promise<void> {
const current = await getNumber(KEYS.positiveSignals);
await AsyncStorage.setItem(KEYS.positiveSignals, String(current + 1));
}
// 依頼してよいか判定し、満たせば「表示機会」を記録してリクエストする
export async function maybeRequestReview(appVersion: string): Promise<boolean> {
const isAvailable = await StoreReview.isAvailableAsync();
if (!isAvailable) return false;
const signals = await getNumber(KEYS.positiveSignals);
if (signals < REQUIRED_SIGNALS) return false;
const promptedVersion = await AsyncStorage.getItem(KEYS.version);
if (promptedVersion === appVersion) return false; // 同一版で二度と出さない
const lastAtRaw = await AsyncStorage.getItem(KEYS.lastPromptAt);
if (lastAtRaw) {
const days = (Date.now() - new Date(lastAtRaw).getTime()) / 86400000;
if (days < MIN_DAYS_BETWEEN) return false;
}
// ここを通過した瞬間だけを「表示機会」として数える
const opp = await getNumber(KEYS.opportunities);
await AsyncStorage.setItem(KEYS.opportunities, String(opp + 1));
await AsyncStorage.setItem(KEYS.lastPromptAt, new Date().toISOString());
await AsyncStorage.setItem(KEYS.version, appVersion);
await StoreReview.requestReview(); // 表示されるかはOSが決める
return true;
}肝心なのは、maybeRequestReview() が true を返した回数を分析基盤に送っておくことです。この「表示機会」の総数と、App Store Connect / Play Console の評価件数の増分を並べれば、見えないはずの表示・投稿の実態を推定できます。