お疲れ様です!IT業界で働くアライグマです!
個人開発している開発関連ニュースのキュレーションサービス「DevPick」のモバイルアプリを、Google Play ストアへ先行リリースする準備を進めていました。有料プランの実装はすでに終わっており、あとはストア側の設定を済ませれば出せる状態です。
ところが、そのストア側の作業を進める段になって、コードとはまったく関係のないところで止まりました。有料プランを出すと、個人開発者である私の自宅住所が公開されてしまう、という問題です。今回は、完成していた課金をリリース直前で畳み、完全無料と広告のモデルへ切り替えた判断の記録を書きます。
有料プランを作り終えてから、住所の問題に気づいた
DevPickのモバイルアプリには、もともと「DevPick Pro」という有料プランを用意していました。無料のまま使う場合はAI要約の閲覧が1日5件までで、記事の一覧には5記事ごとに広告が挟まります。Proにすると、その上限と広告の両方がなくなる、という設計です。
課金の仕組みには RevenueCat を使い、月額480円、年額4,800円、買い切り9,800円の3種類を用意していました。アプリ側にはプランを選んで購入する画面も、機種変更などに備えた「購入履歴の復元」も実装済みです。残っていたのは、Google Play Console に課金アイテムを登録する作業だけでした。
ここで引っかかったのが、有料の販売を行うと必要になる表示です。日本で継続的に有料のデジタルコンテンツを販売する場合、特定商取引法に基づく表記として、事業者の氏名・住所・連絡先を利用者が確認できる形で示す必要があります。法人であれば会社の所在地を書けば済みますが、個人で、かつ自宅以外に事業所を持っていない場合、そこに書ける住所は自宅しかありません。
加えて、Google Play では有料アプリや課金を扱うデベロッパーの情報がストアの掲載内容として扱われます。つまり、課金アイテムを1つ登録するという操作の先に、自宅住所の公開という結果がぶら下がっていました。
技術的には何も詰まっていません。詰まったのは、まだどれだけ売れるかも分からない有料プランのために自宅住所を公開するかどうか、という判断の部分でした。個人開発は家族と同じ家の中で続けているものですから、ここで公開する住所は私だけのものではありません。実装が完成していたぶん、この段階で足が止まるのは正直こたえましたが、判断を先送りにできる問題でもありませんでした。
IT女子 アラ美「課金アイテムを登録しない」だけでは、審査で落ちる
最初に思いついたのは、いちばん横着な手でした。アプリのコードはそのままにして、Google Play Console に課金アイテムを登録しなければいい、というものです。商品が存在しなければ誰も買えないので、有料の販売は発生せず、表示義務も生じない、という理屈です。
しかし、これはアプリの側から見ると具合の悪い状態になります。購入画面はストアに問い合わせて商品情報を取得し、価格や期間を表示してから購入へ進む作りです。商品が1つも登録されていなければ、この問い合わせは「該当なし」で返ってきます。
つまり、利用者から見れば次のような画面になります。
// ストアから取得した商品が空のとき
if (packages.length === 0) {
// 「現在購入可能なプランはありません」
// 「ストアで利用可能なプランが準備中か、提供地域外の可能性があります」
// + 再読み込みボタン
}
この表示自体は、通信が一時的に失敗したときの受け皿としては正しい実装です。ただ、商品を永久に登録しないつもりでこの状態を出し続けるとなると、話が変わります。アプリの中に「Proにアップグレード」という導線があり、押すと購入画面が開き、そこには一生成功しない再読み込みボタンだけが置いてある、ということになるからです。
Google Play の審査では、こうした「動かない機能」は不具合として扱われます。押せるのに何も起きないボタン、たどり着けない画面、常にエラーを出し続ける導線といったものは、リジェクトの典型的な理由です。住所の公開を避けようとして、代わりに審査落ちのリスクを抱え込むのでは意味がありません。
売らないと決めたなら、売る導線ごと消す必要がある、という当たり前の結論にここで行き着きました。



コードを消さず、フラグで畳む
導線を消すといっても、実装したコードを削除するのは避けたいところでした。将来、バーチャルオフィスなどの事業所住所を用意するか、組織としてのアカウントへ移行すれば、住所の問題は解消して課金を出せるようになります。そのときに、いったん消したコードを書き直すのは無駄ですし、消した時点で動作していた事実も失われます。
そこで、有効・無効を1か所で切り替えるフィーチャーフラグを定数として置きました。
/**
* アプリ内課金(DevPick Pro)の有効化フラグ。
*
* 個人デベロッパーの住所公開を回避し、完全無料+広告(Google AdMob)モデルで
* 先行公開するため、デフォルトで false(準備中モード)に設定する。
* 将来的にバーチャルオフィスの導入や組織アカウント移行後に true へ切り替える。
*/
export const ENABLE_IN_APP_PURCHASES = false;
購入画面では、このフラグで画面の中身を丸ごと出し分けるようにしました。false のときはプラン選択・購入ボタン・復元ボタンをすべて描画せず、代わりに「DevPick Pro は近日公開予定です」という予告のカードと、「アプリに戻る」ボタンだけを出します。true に戻せば、これまで作ってきた購入画面がそのまま復活します。
条件をあちこちに散らさず、画面単位・要素単位で分岐の入口を1つに保つのがポイントでした。ボタンだけ隠して購入処理の呼び出しが残っている、といった中途半端な状態を作ると、フラグを戻すときに何が有効で何が無効なのか分からなくなります。
もう1つやったのが、無効側の動作をテストで固定することです。もともとあったテストは、課金が有効な前提で購入や復元が動くことを確認するものでした。そこへ、フラグを無効に固定したテストを別ファイルで追加しています。
// 定数モジュールをモックして、無効側の分岐だけを検証する
jest.mock("../../constants", () => {
const actual = jest.requireActual("../../constants");
return { __esModule: true, ...actual, ENABLE_IN_APP_PURCHASES: false };
});
it("「近日公開予定」が表示され、購入ボタン・プラン選択は表示されない", async () => {
// …予告カードが出ていること、購入導線が無いことを確認する
});
こうしておくと、有効側と無効側の両方が同時にテストの対象として残ります。フラグで切り替える実装は、片方だけを試して満足すると、もう片方がいつの間にか壊れます。畳んだ側も、戻す側も、どちらも動くと確認できる状態にしておきたいところでした。



上限に達した人へ見せる文言が、課金前提のままだった
購入画面を畳んだあと、実際に無効の状態でアプリを触ってみると、そこだけでは終わらないことが分かりました。課金があることを前提に書かれた文章が、アプリのあちこちに散らばっていたのです。
いちばん分かりやすかったのが、AI要約の閲覧が1日5件の上限に達したときの案内でした。もとの文面は「DevPick Proで要約を無制限に閲覧できます。」です。これは、上限に当たった人に対して有料プランを案内するための文章なので、買えない状態では案内として成立しません。
これを「日付が変わると閲覧枠がリセットされます(Proプランは近日公開予定)。」に差し替えました。買ってもらう代わりに、明日また読めるという事実を伝える文章です。同じ場所にあったボタンのラベルも、「Proを見る」から「Pro予定」へ変えています。押した先は同じ画面ですが、そこにあるのが購入導線ではなく予告である以上、ラベルも実態に合わせる必要がありました。
同じ整理を、記事一覧の上部、マイページ、記事の詳細を開いたときのシートに対しても行っています。ボタンに添えるアイコンも、有料プランを示す王冠から、予告であることが伝わるものへ切り替えました。
それから、消したものもあります。マイページと購入画面には「アプリ内でプロフィールを削除しても、Google Playの定期購入は自動では解約されません」という注意書きを置いていました。これは定期購入を契約している人にとっては必要な情報ですが、契約する手段そのものが無い状態では、存在しない契約への警告になってしまいます。フラグが無効の間は、この注意書きも出さないようにしました。
記事一覧のヘッダーに常設していた「Pro」へのアップグレードボタンも、フラグが無効の間は出しません。条件としては次のような形です。
// 「Pro会員ではない」だけでなく「課金が有効である」ことを条件に加える
{!isPro && ENABLE_IN_APP_PURCHASES && (
// Proへのアップグレードボタン
)}
機能を止めるという作業は、その機能を実行するコードを止めることだと思いがちですが、実際にはそれ以上に、その機能があることを前提に書かれた説明文を全部洗い出す作業でした。コードは分岐を1か所にまとめれば済む一方、文章は画面ごとに別々に書かれているので、目で追って拾うしかありません。



規約とプライバシーポリシーが、実装に追いついていなかった
住所の問題に行き当たる少し前、ストア申請の準備として書類まわりを点検していたときに、別のズレも見つかっていました。アプリの実装が先に進んでいて、規約とプライバシーポリシーがそれに追いついていない状態です。
モバイルアプリには、この時点ですでに広告の仕組みと課金の仕組みが組み込まれていました。広告を配信するために端末の広告識別子を扱いますし、課金を通せば購入情報が決済まわりのサービスへ渡ります。ところが、公開していたプライバシーポリシーにはどちらの記載もありませんでした。利用規約の側にも、有料プランの料金や自動更新、解約の方法を定めた条文がありませんでした。
まずいのは、書いていないこと自体よりも、記載場所の二重管理です。同じ内容を書く場所が複数あって、片方だけが更新されていました。Google Play には、どんなデータを収集して誰と共有するかを申告する欄があり、そこには広告と決済のサービスを共有先として書く前提で整理を進めていました。つまり、ストアには「広告識別子を扱う」と申告しながら、自分のサイトのプライバシーポリシーには何も書いていない、という食い違いが生まれます。
この「同じことを2か所に書くとズレる」という現象は、以前Claude CodeとGeminiでスキルを二重管理して破綻しかけた話でも踏んでいます。あのときは開発環境の設定ファイルでしたが、構造としては同じで、片方を直したときにもう片方を直す仕組みが無いと、時間の経過とともに必ず離れていきます。
対応としては、プライバシーポリシーに広告識別子の取得と、決済情報の取り扱い、それらの提供先を明記しました。利用規約には有料プランの条文を新しく起こし、料金とプラン内容はストアの購入画面の表示によること、定期購入は解約しない限り自動更新されること、解約はGoogle Playの設定から利用者自身が行うこと、返金はGoogle Playのポリシーに従うことを定めています。条文を1つ挿入したので、それ以降の条番号も繰り下げました。
あわせて、要件の管理表に「ストアへの申告と、公開しているポリシー・規約の記載を一致させる」という運用上の注意を書き足しました。次に広告や課金まわりへ手を入れる人(といっても私ですが)が、コードだけ直して文書を忘れないようにするためです。
なお、このあと課金そのものを畳んだため、有料プランの条文は当面「提供することがあります」という将来の形で残っています。実態としてはまだ売っていない状態ですが、フラグを戻したときに規約だけが空白という事態は避けられます。



まとめ
個人開発で有料プランを出すというのは、決済を実装してストアに商品を登録する作業だと思っていました。実際には、その手前に「販売者として自分の住所を公開できるか」という、コードとは無関係な関門があります。そこを越えられないと判断したので、先行リリースは完全無料と広告のモデルにしました。
このとき効いたのが、実装を削除せずフラグで畳むという形です。事業所の住所を用意するか、組織としてのアカウントへ移行すれば、定数を1つ切り替えるだけで購入画面は戻ります。無効側の動作もテストで固定してあるので、畳んでいる状態が壊れていないことも確認できます。
一方で、機能を止める作業の大半はコードではありませんでした。上限に達した人への案内、マイページの説明、アップグレードの導線、定期購入に関する注意書きと、その機能があることを前提に書かれた文章が想像以上にあり、そこを実態に合わせて直すほうに時間がかかっています。規約とプライバシーポリシーも含めて、実装と文書が同じ現実を指しているかという点検が、結局いちばん手間のかかる部分でした。
収益という観点では、有料プランを出せない状態はもちろん後退です。それでも、住所を公開してまで急ぐ理由は今の私にはありませんでした。出せる形で先に出しておいて、条件が整ってから戻せばよいと考えています。












