ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-07-04上級

課金済みなのに広告が消えない — StoreKit 2 の Transaction.updates を起動直後に取りこぼす競合を Antigravity と塞いだ記録

起動直後に届く StoreKit 2 のトランザクションを取りこぼす競合を、View 寿命ではなくアプリ寿命にリスナーを固定して塞いだ実装記録。currentEntitlements での照合、finish() の徹底、StoreKitTest での回帰テスト化まで。

StoreKit 24iOS27in-app-purchase2Antigravity308app-dev45

プレミアム記事

「課金したのに広告が消えません」。個人開発で運営している壁紙アプリの問い合わせ欄に、この一文が月に数件、静かに積み上がっていました。決済ログを見ると購入は成立しています。App Store のサンドボックスで再現を試みても、私の手元では毎回きれいに広告が消えます。再現しない不具合ほど、胸の奥に小さな石が残ります。

原因は、購入処理そのものではありませんでした。購入が成立した「その瞬間」ではなく、アプリを次に起動した瞬間に届くトランザクションを、アプリ側が受け取り損ねていたのです。StoreKit 2 の Transaction.updates を、画面(View)の寿命に紐づけて監視していたことが競合の温床でした。

この記事は、その競合をどう突き止め、リスナーをアプリの寿命に固定し直し、最後に Antigravity へ回帰テストの作成を任せて再発を防いだかの記録です。StoreKit 2 で購読や単体購入を扱う個人開発者に、同じ石を胸に残してほしくないという気持ちで書いています。

「決済は成立、UI は据え置き」がなぜ起きるのか

StoreKit 2 では、直接の購入フロー(product.purchase() の戻り値)以外の経路でトランザクションが届きます。自動更新、家族の承認待ち(Ask to Buy)の後日承認、別デバイスでの購入、返金や失効。これらは購入ボタンの戻り値には現れず、Transaction.updates という非同期シーケンスから流れてきます。

問題は配信のタイミングです。Ask to Buy で保護者が承認したトランザクションは、子どもの端末でアプリが閉じられている間に確定します。次にアプリを起動したとき、StoreKit はそのトランザクションを Transaction.updates に流そうとします。ここでリスナーがまだ動いていなければ、権利の付与が後手に回ります。

私の実装は、購読状態を管理する SwiftUI ビューの .task 修飾子の中でリスナーを起動していました。一見きれいに見えます。けれど .task はビューの表示・非表示に連動します。ビューが消えるとリスナーは中断され、別画面から戻ってくるまで Transaction.updates を誰も見ていない空白が生まれます。決済は Apple 側で成立しているのに、アプリ内の権利フラグと広告の出し分けだけが古いまま。これが「課金済みなのに広告が消えない」の正体でした。

StoreKit は未完了トランザクションを保持し、次回起動で再配信します。つまりデータが永久に失われるわけではありません。失われるのは読者の信頼です。「お金を払ったのに変わらない」という体験は、一度きりで課金をやめる十分な理由になります。

リスナーは View ではなくアプリの寿命に固定する

再設計の核心は一つです。トランザクションの監視を、どの画面が表示されているかから完全に切り離し、アプリが生きている限り止まらないタスクにすること。SwiftUI であれば App の初期化時点で、キャンセルされない Task として起動します。

import StoreKit
import SwiftUI
 
@MainActor
final class EntitlementStore: ObservableObject {
    @Published private(set) var hasPremium = false
 
    // アプリの寿命に紐づく監視タスク。View ではなく Store が保持する
    private var updatesTask: Task<Void, Never>?
 
    func startListening() {
        // 二重起動を防ぐ。既に走っていれば何もしない
        guard updatesTask == nil else { return }
 
        updatesTask = Task.detached(priority: .background) { [weak self] in
            // このループはアプリが終了するまで回り続ける
            for await update in Transaction.updates {
                await self?.handle(update)
            }
        }
    }
 
    private func handle(_ result: VerificationResult<Transaction>) async {
        guard case .verified(let transaction) = result else {
            // 署名検証に失敗したトランザクションは権利付与に使わない
            return
        }
        await refreshEntitlements()
        // 権利へ反映したら必ず finish する。これを怠ると再配信が止まらない
        await transaction.finish()
    }
}

要点は Task.detachedApp 起動直後に一度だけ呼ぶことです。私は Appinit()EntitlementStore を生成し、startListening() をその場で呼ぶ形にしました。ビューがどれだけ入れ替わっても、監視ループは一本のまま生き続けます。

for await update in Transaction.updates を回すループは、意図的にキャンセルしません。StoreKit の公式ガイダンスも、この監視はアプリ起動のできるだけ早い段階で始め、アプリの寿命の間ずっと維持するよう求めています。.task 修飾子に頼ると、この「ずっと」が「表示されている間だけ」に縮んでしまいます。

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

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
起動直後に Transaction.updates を取りこぼす競合の正体と、View 寿命ではなくアプリ寿命にリスナーを固定する再設計
currentEntitlements による起動時照合と finish() の徹底で、再配信ループと権利反映の遅延を止める2段構えの実装コード
StoreKitTest の SKTestSession で Ask to Buy と自動更新を再現し、Antigravity に XCTest を書かせて競合を回帰テスト化する手順
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

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

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

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

関連記事

アプリ開発2026-06-23
StoreKit 2 で「課金したのに使えない」と言われたとき — サブスク権利状態のズレを直す運用メモ
StoreKit 2 のサブスクリプションは実装より運用でつまずきます。currentEntitlements と subscription.status のズレ、Transaction.updates の取りこぼし、未 finish トランザクション、復元と返金の扱いを、本番で効いた整流ロジックとともに記録します。
アプリ開発2026-05-26
アプリ内レビュー要求の出し分け条件を Antigravity Editor で 5 アプリ統一した数日の記録
個人開発で運用してきた iOS 壁紙アプリ 5 本の SKStoreReviewController 呼び出し条件を、Antigravity Editor のマルチファイル編集機能でひとつの基準にまとめ直したときの数日間の所感を、コード例とあわせて静かに記録しました。
アプリ開発2026-04-05
StoreKit 2 を「実装して終わり」にしない — 解約通知・復帰オファー・価格実験で購読収益を育てる運用設計
アプリ内サブスクリプションは、実装が終わってからの運用で収益が決まります。App Store Server Notifications v2 の受信設計、コホート解約分析、Promotional Offers、価格実験、Win-back 自動化までを実践コードと運用の実感で解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →