はじめに:「あとでやろう」が通用しなかった

個人でウェルネスアプリ「Paleotribes(パレトラ)」を開発していて、ついに iOS 版を App Store に出そうとしたとき、いきなり壁にぶつかりました。

Apple の審査から、こういう趣旨のフィードバックが来たのです。

「デジタルコンテンツや機能への課金は App 内では In-App Purchase を使う必要があります」

パレトラは Web 側で Stripe を使った課金構成を組んでいました。iOS アプリにはまだ IAP を実装していなかった。「まずは最小構成で公開して、課金周りはあとで整備しよう」という判断だったのですが、Apple はそれを通してくれませんでした。

この記事は、そのリジェクトにどう向き合い、最終的にどういう設計に落ち着いたかの記録です。同じように「Web 側に Stripe がある状態で iOS アプリを出そうとしている」個人開発者の参考になれば幸いです。


なぜリジェクトされたのか、本質を理解するまで

最初はリジェクトの意味をちゃんと掴めていませんでした。「IAP を入れれば通るんでしょ」と軽く思っていたのです。

でも整理すると、Apple が問題にしていたのは単なる実装不備ではなく、課金チャネルと権限付与の整合性でした。

App Store Connect の履歴を見ると、リジェクトが2回入っています。

パレトラ App Store Connect の審査履歴。Rejected が2回記録されている

Apple から見た状況をまとめるとこうです。

  • Web で購入できるプレミアムプランがある
  • iOS アプリでも同じアカウントを使ってログインできる
  • つまり iOS 上でも有料機能にアクセスできる可能性がある

この構図がある以上、「iOS アプリ内に IAP が提供されていない」という状態は、Apple のガイドラインに抵触するとみなされます。

Web で払った権利を iOS で使えるなら、iOS でも同じ機能を IAP で買える手段を用意しなければならない——これが本質でした。


最初に考えた逃げ道と、なぜやめたか

いくつかの「逃げ方」を考えました。正直に書いておきます。

逃げ道①:App Review に説明して押し切る

「iOS アプリでは有料機能を提供していない」と説明して審査を通す案です。実装コストはゼロ。でもこれは弱い。

Web で買ったアカウントと iOS アプリが同一なのに「iOS では有料機能を提供していない」という説明は、審査員から見ると矛盾に見えます。実際に突っ込まれたとき答えられません。

逃げ道②:iOS では常に free 扱いにする

サーバー側で「iOS クライアントには free の権限しか返さない」という制御を入れる案です。技術的には実装できます。短期的に審査を通る可能性もある。

ただし問題があります。

  • Web でプレミアムを買ったユーザーが iOS を使うと権限がない、という不自然な体験になる
  • 「同じアカウントなのに端末ごとに権限が違う」という設計は将来の負債になる
  • 審査でさらに深掘りされたとき説明できない

審査を一度すり抜けても、長期的に安全な構成ではないと判断してやめました。


辿り着いた核心:「どこで払ったか」と「何を許可するか」は別の話

逃げ道を塞いでいくうちに、重要な設計上の整理ができました。決済手段と権限判定を分離するという考え方です。

  • Stripe は Web 課金のイベントソース
  • Apple IAP / RevenueCat は iOS 課金のイベントソース
  • アプリで実際に使う権限は、サーバーが統合的に判断する

「どこで払ったか」はシステムの都合であって、ユーザーには関係ない話です。ユーザーから見れば「自分はプレミアムを買っている」という事実があるだけ。それを一元管理する責任をサーバーに持たせる——これが今回の設計の中心になりました。


最終的に採った構成

Web 課金:Stripe を継続、iOS 課金:RevenueCat 経由で Apple IAP を実装、権限判定:サーバーで統合という構成に落ち着きました。

[Web]
ユーザー → Stripe Checkout / Customer Portal
         → Webhook
         → DB にサブスク状態を反映

[iOS]
ユーザー → Apple In-App Purchase
         → RevenueCat
         → DB / サーバー連携に反映

[Server]
Stripe の状態 + RevenueCat の状態を統合
↓
getEntitlementsForUser()
↓
free / premium および機能ごとの entitlement を返す

[Client(iOS・Web 共通)]
サーバーが返した entitlements に従って UI・機能を制御

クライアントは決済手段を直接信頼しない。サーバーが唯一の正とする設計です。


なぜ RevenueCat を選んだか

Apple のレシート検証やサブスク状態の同期を自前で厚く持つことは、個人開発のコストとしては割に合いません。RevenueCat を使うと次の利点があります。

  • Apple のレシート検証を任せられる
  • サブスク状態(アクティブ、期限切れ、復元済みなど)の管理がシンプルになる
  • Webhook で自前 DB への反映も整理しやすい
  • 将来 Android を追加する際も対応しやすい

Web は Stripe で動いている基盤をそのまま活かし、iOS だけ RevenueCat を使うという分業が自然でした。


両チャネル有効時のルール

Stripe と IAP の両方が有効になったとき(Web と iOS 両方で購入した場合など)の扱いも整理しました。

シンプルに、currentPeriodEnd がより先の方を優先するというルールにしました。ユーザー体験を壊さず、実装が複雑にならず、将来読んだときにも意図が明確。個人開発の段階では複雑な優先度ロジックを書くより、このくらいシンプルなルールで十分です。


App Review を通すために実際にやったこと

設計が整っても、審査員に伝わらなければ通りません。App Review Notes の整備が思ったより重要でした。

審査ノートに明記した内容:

  • プランの名称と月額料金
  • 設定画面のどこに購入導線があるか
  • Restore Purchases ボタンの場所
  • Terms / Privacy への導線がどこにあるか

Apple の審査では、機能が実装されているだけでなく「審査員がたどれること」が重要です。購入導線や復元ボタンを設定画面にまとめて常時表示することで、審査員が確認しやすくなります。


この対応を通じて整理できたこと

結果として、この問題に向き合ったことで設計がきれいになりました。逃げ道を選んでいたら、「iOS は always free」という不自然な構成を抱えたまま先に進んでいたはずです。

迷ったタイミングに正しく向き合うと、設計の質が上がるというのは、個人開発をやっていると繰り返し感じることです。


まとめ

  • Web 課金(Stripe 等)がある状態で iOS アプリを出す場合、Apple IAP の要否は早めに確認する
  • 「説明で押し切る」「iOS では free にする」は短期対応としては考えられるが、長期的に脆い
  • 「決済手段」と「権限判定」を分離する設計にするとスッキリする
  • RevenueCat は個人開発での iOS IAP 運用に向いている
  • App Review Notes には購入・復元・利用規約の導線を明確に書く

小さな個人開発でも、設計の判断を記録として残しておくと、後で振り返ったときに役立ちます。今回はそれを記事にしてみました。


Paleotribes(パレトラ)は習慣・ウェルネス管理のモバイルアプリです。現在 iOS 版を公開中です。