2026 年 6 月 30 日に始めて、8 月 15 日に App Store へ出しました。Google Play はその前日、8 月 14 日。47 日です。審査リジェクトの往復ひとつを含めて。
ahha — 日本の宝くじ(LOTO6・LOTO7)の統計分析アプリです。過去の当選データから出現傾向を見せ、番号の組み合わせを作り、販売締切前に通知を出す。無料で使えて一部機能が月額購読で開く構造です。
技術的に特別なことは何もありません。ところが特別でないものが次々に足を止めました。この記事はその地雷の一覧です。各項目は別の記事で詳しく扱っており、ここでは「何を踏んだか」だけを並べます。
構成
アプリ React Native 0.86 + Expo SDK 57 (Bare Workflow) + TypeScript 状態 Apollo Client + Reactive Var 保存 MMKV データ 静的 CSV(バンドル初期値 + リモート delta) サーバー 共有ホスティング(静的サイト + PHP CLI スクレイパー + cron) 決済 RevenueCat(月額購読) 広告 AdMob(アプリ)・AdSense(Web) 通知 ローカル通知 分析 Firebase Analytics + Crashlytics 配信 日本 1 か国
Web 版も同じコードベースで動きます。ランディングページとアプリのプレビューがひとつのプロジェクトに入っています。
1. ストア審査
ATT を申告だけして実装していなかった
Info.plist に NSUserTrackingUsageDescription のキーだけがあり、実際の権限要求コードがありませんでした。Apple はキーがあれば「このアプリは ATT を使う」と判定するので、要求がないこと自体がリジェクト事由になります。
広告 SDK を入れるときに慣例でキーだけ置いておくことが多く、それがそのまま地雷になります。年齢等級の設問でも同時に落ちました。宝くじ予想アプリは、アプリ内で賭けるかどうかではなく実際に金銭が賭けられる対象に関わる情報・道具を提供するかが基準です。
サンドボックスで解約 → 再購入を繰り返せない
iOS は Apple サーバーに残ったレシートが自動同期され、消した購読が生き返ります。「購入履歴を消去」の反映は最大で数十日かかります。決済フロー自体の確認までにとどめ、残りは本番の実決済に回すのが正解でした。
購読アプリの審査には準備物が多い
Paywall 画面の価格・期間・自動更新条件・法的リンク 3 種・復元ボタン、解約導線、アプリコンテンツの設問 11 項目、データセーフティの申告、提出物 3 項目の確認。
2. 購読・決済
購読が終わったのにアプリは「継続中」と言った
期限切れはストアサーバー通知の対象外です。解約・更新・支払い問題はプッシュが来ますが、期限切れは時間が経って起きることなのでプッシュすべきイベントがありません。そのため SDK のローカルキャッシュに期限切れ直前のスナップショットが残り、起動時の照会がその値を返します。
アプリを二度直したのに直らなかった
原因がアプリの外にありました。Google Play → RevenueCat の通知経路(Pub/Sub)そのものが繋がっていませんでした。権限設定で「Pub/Sub 管理者」ではなく「Pub/Sub Lite 管理者」を選んでさらに迷いました。まったく別のサービスです。
作ったお知らせを見られるユーザーがほとんどいなかった
プラン終了のお知らせを作ってチェックリストに「完了」を付けたのに、その通知を読むコードが「その他」タブの一か所にしかありませんでした。特に支払い手段の問題はカードを直せば復旧するので、それを伝えられないと問い合わせに直結します。
3. Expo Bare Workflow
app.json を直しても何も変わらなかった
ios/android フォルダを直接管理している状態(prebuild 不使用)では app.json がネイティブに反映されません。公式文書に明記されているのに見落とし、スプラッシュのロゴサイズをひとつ変えるのに四時間を使いました。
ネイティブモジュールが Web を三通りに殺した
Skia で作ったコンポーネントが Web で画面全体を黒くし、動的 import が deprecated なモジュールを引き込んでアプリの起動を殺し、ATT のモジュールが import 行でクラッシュしました。三つ目が特に罠です。Platform.OS の分岐を入れても効きません。import が先に評価されるからです。
pod install が成功したのにリンクで落ちた
Podfile.lock に正常に登録され、コンパイルも通ったのに ld: symbol(s) not found。Pods フォルダと Podfile.lock を消して入れ直すと通りました。バージョン変更のときは Pods を消さないのが原則ですが、新規ネイティブモジュールの追加は例外でした。
4. インフラ
Akamai が 403 を返し続けた
TLS ハンドシェイクの指紋で弾いていたので、ヘッダをどれだけ飾っても通りませんでした。共有ホスティングなので Python のバージョンも上げられず root もない。方向を五回変えた末に PHP + curl-impersonate のバイナリで解けました。「PHP では根本的に無理」と断定するところでしたが、違いました。
デプロイして三時間後にサイトが消えた
同じサーバーの別プロジェクトが、上位ドメインのドキュメントルートを rsync --delete で配信していました。このホスティングはサブドメインが上位ドキュメントルートの下位フォルダなので、丸ごと消えます。ひとつのプロジェクトの中だけを漁っても絶対に見つからない原因でした。
スポンサーリンク
スポンサーリンク
47 日を振り返って
コードよりコードの外で詰まった
12 項目のうち純粋なコードのバグはいくつもありません。残りはストアの方針、サーバー設定、ビルドツール、インフラ構造 — コードの外のものです。
個人開発で「機能は全部作ったのにリリースできない」という状況がなぜ起きるのか、今なら分かります。作ることと出すことは別の仕事です。
「直したのに直らない」が二度なら外を見る
- アプリを二度直したのに直らない → サーバーの通知経路が切れていた
- 自分のプロジェクトをどれだけ漁っても原因がない → 隣のプロジェクトが消していた
app.jsonを三度直しても変化がない → そもそも読まれていないファイルだった
全部同じパターンです。前提を壊さずに時間を使いました。
「不可能」はたいてい調査不足
「PHP では無理」「共有ホスティングでは無理」— どちらも間違いでした。技術的な不可能を宣言する前に実際の使用例を検索すれば、たいてい先に当たった人がいて解法もあります。
現場の感覚が文書より正しいことがある
app.json の件で「それだけ変えても駄目そうだが」という指摘を受けたのに、「公式文書にこうあるので通るはずです」と答えてさらに四時間を使いました。文書が前提する環境と実際の環境が違うとき、文書は間違った答えを返します。
AI とペアで作りながら
このプロジェクトはほとんどの作業を AI と対話しながら進めました。セッションを分けて調査 → 処方 → 検証を繰り返す方式です。いくつか規約が必要でした。
推定で空白を埋めない。分からないことを知っているふりで埋めると、それが後で地雷になります。「確認できていない」と言うほうがいい。
セッションが変わると記憶が消える。プロジェクトのコンテキスト文書をひとつ置いて毎セッション更新しました。失敗の記録(何をどう間違えたか)を積み上げ、この記事の 12 項目のほとんどがそこから出ています。
実物を先に見る。文書に書かれたものと実際のコードが違えばコードが正しい。調査なしに処方を出すと外します。
この三つは、実は AI と働くときだけの規約ではありません。ひとりでやるときも、二か月前の自分は別人です。
これから
リリースは始まりでしかない、という言葉のとおりです。実際のユーザーが入ってくれば、サンドボックスでは見えなかったものが出てくるはずです。
今わかっている繰り越し項目は、ATT 対応が Android ビルドにまだ入っていないこと、AdMob のアプリ確認(app-ads.txt)のクロール反映待ち、本番の実決済フローの検証です。
そして次のアプリを準備しています。ドメインは違いますが枠組みは同じです。React Native、無料 + 月額購読、日本市場。この 47 日で踏んだ地雷を次は踏まずに済むなら、この記事を書いた元は取れた勘定です。
スポンサーリンク
スポンサーリンク
この記事について
「47 日」は着手日(2026 年 6 月 30 日)と App Store 公開日(2026 年 8 月 15 日)の間の日数であり、実作業時間ではありません。ダウンロード・売上のデータは公開直後のためなく、成果の話はしていません。「AI とペアで」の部分は開発の進め方の記述であり、生産性の向上度を定量化したものではありません。













コメント