RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-04中級

Rork で「Restore Purchases」が動かないとリジェクトされる — App Store審査3.1.1の落とし穴と修正手順

Rorkで作ったサブスクリプションアプリがApp Store審査でリジェクトされる原因の上位に「Restore Purchasesボタンの不備」があります。Apple Guideline 3.1.1の正しい解釈と、expo-in-app-purchases / react-native-iap での実装パターンを解説します。

Rork515In-App Purchase2Restore PurchasesApp Store審査8Guideline 3.1.1expo-in-app-purchasesreact-native-iapサブスクリプション63

サブスクリプションアプリを Rork で作って TestFlight には通った、けれど審査リジェクトの通知が届いた。理由を読むと「Guideline 3.1.1 — In-App Purchase」と書いてあって、本文には「ユーザーがアカウントを復元する手段が見当たらない」というメッセージ。私はこのリジェクトを過去に3回受けたことがあって、毎回原因は同じところにあります。

意外なほど多くの個人開発者が「Restore Purchases ボタンを実装したつもり」で詰まっています。ボタンは画面に置いてあるのに、審査チームは「動いていない」と判定して返してくる。今回はその原因を、Rork で生成された React Native コードベースを前提に、最短で直す手順としてまとめます。

なぜ Restore Purchases が必須なのか

Apple Developer Program License Agreement と App Store Review Guideline 3.1.1 では、サブスクリプションや非消費型 IAP を販売するアプリに、購入を復元する手段の提供を義務付けています。これは法律上の保護というよりは、ユーザー体験の保護です。

具体的な利用シーンを想像すると分かりやすいです。ユーザーが iPhone を機種変更した、アプリを一度削除して再インストールした、あるいは家族で同じ Apple ID を共有していて別の端末で課金ステータスを引き継ぎたい。こうした場面で「もう一度お金を払ってください」と言われたら、誰でも怒ります。Apple は「すでに支払った購入を、ユーザーがいつでも追加課金なしに再有効化できるボタン」を要求しています。

ここで重要なのは、Apple にとっての「動いている」の定義です。ボタンを押すと StoreKit を呼び出して、Apple ID に紐付いたアクティブなトランザクションをアプリ側に渡し、そのトランザクションを根拠にアプリが会員権限を再付与します。この一連のフローが画面遷移なしに完結することを審査で確認します。ボタンが「Coming Soon」のアラートを出すだけでは、当然リジェクトです。

リジェクトされる4つの典型パターン

実際にリジェクトされたコードを見てきた経験から、原因はだいたい4種類に分けられます。

ボタンが画面のどこにもありません。 これは初心者によくあるパターンで、Stripe の決済 UI を React Native アプリに移植したまま IAP の仕様を読まずに公開すると起こります。Apple は「ボタンを設定画面または購入画面のどちらかに配置すること」を要求しています。

ボタンはあるが API を呼んでいありません。 Rork の AI に「Restore Purchases ボタンを作って」とだけ伝えると、見た目だけ整って onPress で Alert.alert("Restored!") を出すだけのコードを生成することがあります。これでは StoreKit と通信していません。

API は呼ぶが結果を反映していありません。 getAvailablePurchases() を呼んで配列を取得しているのに、取得したトランザクションを基にアプリ側の権限フラグ(例: AsyncStorage の is_premium)を更新していないケース。ユーザーから見ると「ボタンを押しても何も起きない」アプリです。

Sandbox 環境でしかテストしていありません。 TestFlight で動いているからといって、本番リリース後のレシート検証フローを通っていないことがあります。Sandbox レシートは sandbox.itunes.apple.com に送るのに対して、本番は buy.itunes.apple.com に送る必要があり、ここを切り替えていないと本番でだけ Restore が失敗します。

expo-in-app-purchases での実装

Rork で生成された Expo プロジェクトで IAP を扱う場合、選択肢は expo-in-app-purchases(Expo SDK 49 まで標準サポート、現在は extras 扱い)か react-native-iap(コミュニティ製、機能豊富)の2択になります。新規プロジェクトなら後者を推奨しますが、すでに expo-in-app-purchases で書かれているコードを修正する場合の最小実装を示します。

// app/utils/restorePurchases.ts
import * as InAppPurchases from 'expo-in-app-purchases';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Alert } from 'react-native';
 
export async function restorePurchases(): Promise<boolean> {
  try {
    // StoreKit との接続を確認
    await InAppPurchases.connectAsync();
 
    // Apple ID に紐付いた購入履歴を取得
    const { results, responseCode } = await InAppPurchases.getPurchaseHistoryAsync({
      useGooglePlayCache: false, // iOS では無視されるが明示的に設定
    });
 
    if (responseCode !== InAppPurchases.IAPResponseCode.OK) {
      throw new Error(`Restore failed with code ${responseCode}`);
    }
 
    // アクティブなサブスクリプションを抽出
    const activeSubscription = results?.find(
      (purchase) =>
        purchase.productId === 'com.example.app.premium_monthly' &&
        purchase.transactionReceipt &&
        !purchase.acknowledged === false
    );
 
    if (activeSubscription) {
      // 権限フラグを更新(最も忘れられやすい部分)
      await AsyncStorage.setItem('is_premium', 'true');
      await AsyncStorage.setItem('premium_restored_at', new Date().toISOString());
      Alert.alert('復元完了', 'サブスクリプションを復元しました。');
      return true;
    } else {
      Alert.alert('復元する購入なし', 'この Apple ID で購入された有効なサブスクリプションが見つかりませんでした。');
      return false;
    }
  } catch (error) {
    console.error('[restorePurchases] error:', error);
    Alert.alert('エラー', '購入の復元に失敗しました。ネットワークを確認してもう一度お試しください。');
    return false;
  } finally {
    await InAppPurchases.disconnectAsync();
  }
}

このコードのポイントは、AsyncStorage.setItem('is_premium', 'true') の行が必ず含まれていることです。getPurchaseHistoryAsync が成功しても、アプリ内で会員権限のフラグを更新しない限り、ユーザーから見ると「ボタンを押しても何も変わらない」状態のままです。

react-native-iap での実装

新規プロジェクトや、より細かい制御が必要な場合は react-native-iap を使います。こちらの方が現在は活発にメンテナンスされていて、StoreKit 2 の async/await にも対応しています。

// app/utils/restorePurchases.ts
import {
  initConnection,
  endConnection,
  getAvailablePurchases,
  finishTransaction,
  Purchase,
} from 'react-native-iap';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Alert } from 'react-native';
 
const PRODUCT_IDS = {
  PREMIUM_MONTHLY: 'com.example.app.premium_monthly',
  PREMIUM_YEARLY: 'com.example.app.premium_yearly',
};
 
export async function restorePurchases(): Promise<boolean> {
  try {
    await initConnection();
 
    // Apple ID 配下のすべての有効な購入を取得
    const purchases: Purchase[] = await getAvailablePurchases();
 
    // サブスクリプション系の productId に絞り込み
    const activePurchases = purchases.filter((p) =>
      Object.values(PRODUCT_IDS).includes(p.productId)
    );
 
    if (activePurchases.length === 0) {
      Alert.alert('復元なし', 'この Apple ID で購入された有効なサブスクリプションが見つかりませんでした。');
      return false;
    }
 
    // 最新の購入を権限の根拠にする
    const latestPurchase = activePurchases.sort(
      (a, b) => (b.transactionDate ?? 0) - (a.transactionDate ?? 0)
    )[0];
 
    // サーバー側でレシート検証する場合はここで送信
    // const verified = await verifyReceiptOnServer(latestPurchase.transactionReceipt);
 
    // アプリ内の権限フラグを更新
    await AsyncStorage.setItem('is_premium', 'true');
    await AsyncStorage.setItem('premium_product_id', latestPurchase.productId);
 
    // トランザクションを完了状態にする(重要: これを忘れると同じ購入が何度も復元イベントとして来る)
    await finishTransaction({ purchase: latestPurchase, isConsumable: false });
 
    Alert.alert('復元完了', 'プレミアム機能をご利用いただけます。');
    return true;
  } catch (error: any) {
    console.error('[restorePurchases] error:', error);
    Alert.alert('エラー', error?.message ?? '購入の復元に失敗しました。');
    return false;
  } finally {
    await endConnection();
  }
}

finishTransaction の呼び出しを忘れると、次回起動時にも同じトランザクションが「未処理」として降ってきて、ユーザーが Restore を押すたびに同じ購入を再認識する挙動になります。これは見た目の不具合だけでなく、Apple の審査チームがランダムに何度も Restore を押して挙動を確認するため、リジェクト理由になることがあります。

設定画面に必ずボタンを置く

実装ができたら、ユーザーがすぐ見つけられる場所に Restore Purchases ボタンを配置します。Apple が推奨する位置は2つあります。

ひとつはサブスクリプション購入画面の中。新規ユーザーが購入を検討するときに「すでに購入済みの場合はこちら」という選択肢を見せます。もうひとつは設定画面のサブスクリプション関連項目。これが審査でチェックされる主要な場所です。

// app/(tabs)/settings.tsx
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { restorePurchases } from '@/utils/restorePurchases';
 
export default function SettingsScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.sectionTitle}>サブスクリプション</Text>
 
      <TouchableOpacity
        style={styles.row}
        onPress={() => restorePurchases()}
        accessibilityLabel="購入を復元する"
        accessibilityRole="button"
      >
        <Text style={styles.label}>購入を復元 (Restore Purchases)</Text>
      </TouchableOpacity>
    </View>
  );
}

accessibilityLabelaccessibilityRole を必ず指定してください。Apple の審査チームは VoiceOver でアプリを操作することがあり、ラベルがない要素は「機能していない」と判定されることがあります。

Sandbox での確認手順

実装が終わったら、本物の課金が走らない Sandbox 環境でテストします。手順は次の通りです。

まず App Store Connect の「ユーザーとアクセス」から Sandbox テスター用のメールアドレスを作成します。実機の「設定 → App Store → サンドボックスアカウント」にこのアカウントでサインインしておきます。本来の Apple ID とは別のアカウントが必要なので、Gmail のエイリアス機能(yourname+sandbox1@gmail.com のような書き方)を使うと管理しやすくなります。

次に TestFlight ではなく、Xcode から直接実機にビルドします。TestFlight ビルドだと一部のエッジケース(特に解約後の挙動)がテストしづらいためです。アプリを起動して購入フローを通し、一度購入したあとアプリを削除して再インストール、Restore Purchases ボタンを押して権限が戻ることを確認します。

ここで重要な確認ポイントがあります。Sandbox では月額サブスクリプションが3分で1ヶ月分の更新サイクルとして動きます。短時間で「更新 → 解約 → 復元」のシナリオを再現できるので、エッジケースを潰すには絶好のタイミングです。

リジェクト後の再申請メッセージ

もし運悪くリジェクトされた場合、再申請時のレビューノート(Resolution Center への返信)に次のメッセージを添えると、再審査がスムーズに進みやすくなります。

Thank you for the feedback. We have implemented the Restore Purchases functionality in the Settings screen. The button is located at Settings → Subscription → Restore Purchases. Tapping the button will call StoreKit's getAvailablePurchases() and restore active subscriptions tied to the user's Apple ID. We have tested this with sandbox accounts and confirmed that the user's premium status is correctly re-activated after a fresh install.

具体的な配置場所と動作内容を明記することで、審査担当者が迷わずに再確認できます。「修正しました」だけでは再リジェクトの確率が上がるので、「どこに」「何を」実装したかを必ず添えてください。

関連記事

実装後に他のリジェクト要因も潰しておきたい方は、Rork アプリが App Store 審査で3回リジェクトされた理由と対処法を併せて読むと、よくあるリジェクトパターン全般を把握できます。Privacy Manifest 関連のリジェクト対応はPrivacy Manifest API Declaration リジェクト修正ガイドに詳しくまとめてあります。

StoreKit 2 で本格的にサブスクリプション基盤を作る場合は、Rork Max で StoreKit 2 の In-App Purchase を実装する完全ガイドが、Family Sharing の挙動を含めた本番運用はFamily Sharing で IAP が消える4つのトリガーが参考になります。

書籍で体系的に

全体を振り返って

Restore Purchases は「ボタンを置く」だけでは終わらず、StoreKit から取得した購入情報を根拠にアプリ内の権限フラグを更新するところまでが最低条件です。今日からできる一歩としては、現在のコードで getAvailablePurchases の返り値を AsyncStorage に反映する処理が入っているかを確認してみてください。入っていなければ、上記の実装を参考に1時間以内に修正できます。次の審査リジェクトを未然に防ぐ、最も投資対効果の高い修正のひとつです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-07-11
端末の時計は動かせる — 日替わり機能とトライアル判定を時刻操作から守る設計
端末の時計を進めるだけで日替わり特典を先取りできてしまう——壁紙アプリのログで見つけた実例を起点に、壁時計・単調時計・サーバー時刻の3層で時刻源を使い分け、時計の巻き戻しにも耐える日替わりロジックとトライアル判定の設計をまとめました。
開発ツール2026-06-25
機内モードで開くと有料会員にペイウォールが出る — RevenueCat の権限をオフラインでも保つキャッシュ設計
電波の弱い場所でアプリを開いた有料会員に、一瞬ペイウォールが見えてしまう。RevenueCat の customerInfo がオフライン起動で揺れる仕組みと、最後の正解を信頼期限つきでキャッシュして権限を維持する設計を、Expo アプリの動くコードで整理します。
開発ツール2026-06-14
Rork で出したアプリが ITMS-91053 で弾かれたら — Privacy Manifest と Required Reason API の実務
Rork が生成する Expo アプリを App Store に提出すると、コードに覚えがなくても Privacy Manifest 関連の警告で止まることがあります。Required Reason API と SDK 側のマニフェスト漏れの両方を、提出前に潰す手順をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →