見出し画像

Swift | StoreKit2で自動更新のサブスク課金を実装

StoreKit 2は、iOSアプリ内での購買(特にサブスクリプション)の管理を簡素化するために導入されたフレームワークで、Appleが提供しています。Auto-Renewable Subscription(自動更新型サブスクリプション)は、一定の期間が経過すると自動的に更新されるサブスクリプションタイプで、アプリ内でよく利用されます。

自動更新のサブスク課金を実装するコード

以下のファイルは、StoreKit 2を使用したサブスクリプション機能の実装に必要なファイルを示しています。

各ファイルの主な機能を説明します:

  • StoreKit2App.swift

    1. メインアプリケーションのエントリーポイント。
      ContentViewをルートビューとして設定しています。


//  StoreKit2App.swift
import SwiftUI

@main
struct StoreKit2App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
  • ContentView.swift

    1. アプリのメインビュー。

      • StoreVMを@StateObjectとして保持

      • サブスクリプションの状態に応じて表示を切り替え

        • 期限切れ/取り消し時: 再購入を促すメッセージ

        • 未購入時: SubscriptionViewを表示

        • 購入済み: "Premium Content"を表示

//
//  ContentView.swift
//

import SwiftUI
import StoreKit

struct ContentView: View {
    @StateObject var storeVM = StoreVM()
    var body: some View {
        VStack {
            // what the fuck is this
            if let subscriptionGroupStatus = storeVM.subscriptionGroupStatus {
                if subscriptionGroupStatus == .expired || subscriptionGroupStatus == .revoked {
                    Text("Welcome back, give the subscription another try.")
                    //display products
                }
            }
            if storeVM.purchasedSubscriptions.isEmpty {
                SubscriptionView()
                
            } else {
                Text("Premium Content")
            }
        }
        .environmentObject(storeVM)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

  • StoreVM.swift

    1. StoreKitの主要なロジックを管理するViewModel。
      主な機能:

      • 製品情報の取得(requestProducts)

      • 購入処理(purchase)

      • トランザクション監視(listenForTransactions)

      • 購入状態の更新(updateCustomerProductStatus)

      • トランザクションの検証(checkVerified)

//
//  StoreVM.swift
//

import Foundation
import StoreKit

//alias
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo //The Product.SubscriptionInfo.RenewalInfo provides information about the next subscription renewal period.
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState // the renewal states of auto-renewable subscriptions.


class StoreVM: ObservableObject {
    @Published private(set) var subscriptions: [Product] = []
    @Published private(set) var purchasedSubscriptions: [Product] = []
    @Published private(set) var subscriptionGroupStatus: RenewalState?
    
    private let productIds: [String] = ["subscription.weekly"]
    
    var updateListenerTask : Task<Void, Error>? = nil

    init() {
        
        //start a transaction listern as close to app launch as possible so you don't miss a transaction
        updateListenerTask = listenForTransactions()
        
        Task {
            await requestProducts()
            
            await updateCustomerProductStatus()
        }
    }
    
    deinit {
        updateListenerTask?.cancel()
    }
    
    
    
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            //Iterate through any transactions that don't come from a direct call to `purchase()`.
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    // deliver products to the user
                    await self.updateCustomerProductStatus()
                    
                    await transaction.finish()
                } catch {
                    print("transaction failed verification")
                }
            }
        }
    }
    
    
    
    // Request the products
    @MainActor
    func requestProducts() async {
        do {
            // request from the app store using the product ids (hardcoded)
            subscriptions = try await Product.products(for: productIds)
            print(subscriptions)
        } catch {
            print("Failed product request from app store server: \(error)")
        }
    }
    
    // purchase the product
    func purchase(_ product: Product) async throws -> Transaction? {
        let result = try await product.purchase()
        
        switch result {
        case .success(let verification):
            //Check whether the transaction is verified. If it isn't,
            //this function rethrows the verification error.
            let transaction = try checkVerified(verification)
            
            //The transaction is verified. Deliver content to the user.
            await updateCustomerProductStatus()
            
            //Always finish a transaction.
            await transaction.finish()

            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }
    
    func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        //Check whether the JWS passes StoreKit verification.
        switch result {
        case .unverified:
            //StoreKit parses the JWS, but it fails verification.
            throw StoreError.failedVerification
        case .verified(let safe):
            //The result is verified. Return the unwrapped value.
            return safe
        }
    }
    
    @MainActor
    func updateCustomerProductStatus() async {
        for await result in Transaction.currentEntitlements {
            do {
                //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
                let transaction = try checkVerified(result)
                
                switch transaction.productType {
                    case .autoRenewable:
                        if let subscription = subscriptions.first(where: {$0.id == transaction.productID}) {
                            purchasedSubscriptions.append(subscription)
                        }
                    default:
                        break
                }
                //Always finish a transaction.
                await transaction.finish()
            } catch {
                print("failed updating products")
            }
        }
    }

}


public enum StoreError: Error {
    case failedVerification
}

  • Subscription.storekit

    1. StoreKitのテスト用設定ファイル。

      • 週間サブスクリプション(subscription.weekly)の定義

      • 価格:10.00

      • 3日間の無料トライアル期間

  • Storekitファイルを作成方法

    • Xcodeでnew file template→拡張子が.storekitのファイルを作成する

    • Subscription.storekitのファイルをクリックし以下の項目を設定する

      • 名前:Pro plan, 〇〇+, Gold Planなど

      • 価格 : $10

      • 無料トライアル期間:3日〜

      • 課金スパン:週次、月次、年次など

{
  "appPolicies" : {
    "eula" : "",
    "policies" : [
      {
        "locale" : "en_US",
        "policyText" : "",
        "policyURL" : ""
      }
    ]
  },
  "identifier" : "3406A955",
  "nonRenewingSubscriptions" : [

  ],
  "products" : [

  ],
  "settings" : {
    "_failTransactionsEnabled" : false,
    "_storeKitErrors" : [
      {
        "current" : null,
        "enabled" : false,
        "name" : "Load Products"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Purchase"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Verification"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "App Store Sync"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Subscription Status"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "App Transaction"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Manage Subscriptions Sheet"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Refund Request Sheet"
      },
      {
        "current" : null,
        "enabled" : false,
        "name" : "Offer Code Redeem Sheet"
      }
    ]
  },
  "subscriptionGroups" : [
    {
      "id" : "B4E3A27E",
      "localizations" : [

      ],
      "name" : "Subscription",
      "subscriptions" : [
        {
          "adHocOffers" : [

          ],
          "codeOffers" : [

          ],
          "displayPrice" : "10.00",
          "familyShareable" : false,
          "groupNumber" : 1,
          "internalID" : "94D41436",
          "introductoryOffer" : {
            "internalID" : "D1DF5A2E",
            "paymentMode" : "free",
            "subscriptionPeriod" : "P3D"
          },
          "localizations" : [
            {
              "description" : "",
              "displayName" : "",
              "locale" : "en_US"
            }
          ],
          "productID" : "subscription.weekly",
          "recurringSubscriptionPeriod" : "P1W",
          "referenceName" : "Weekly",
          "subscriptionGroupID" : "B4E3A27E",
          "type" : "RecurringSubscription",
          "winbackOffers" : [

          ]
        }
      ]
    }
  ],
  "version" : {
    "major" : 4,
    "minor" : 0
  }
}
  • SubscriptionView.swift

  • -サブスクリション購入用のUI。
    機能:

    • 利用可能なサブスクリプションの表示

    • 価格と説明の表示

    • 購入ボタンとその処理

    • 購入状態の管理(isPurchased)


//  SubscriptionView.swift

import SwiftUI
import StoreKit

struct SubscriptionView: View {
    @EnvironmentObject var storeVM: StoreVM
    @State var isPurchased = false

    var body: some View {
        Group {
            Section("Upgrade to Premium") {
                ForEach(storeVM.subscriptions) { product in
                    Button(action: {
                        Task {
                            await buy(product: product)
                        }
                    })
                    {
                        VStack {
                            HStack {
                                Text(product.displayPrice)
                                Text(product.description)
                            }
                            Text(product.description)
                        }.padding()
                    }
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.blue)
                    .cornerRadius(15.0)
                }
            }
        }
    }
    
    func buy(product: Product) async {
        do {
            if try await storeVM.purchase(product) != nil {
                isPurchased = true
            }
        } catch {
            print("purchase failed")
        }
    }
}

struct SubscriptionView_Previews: PreviewProvider {
    static var previews: some View {
        SubscriptionView().environmentObject( StoreVM())
    }
}

このコードベースは、iOS/SwiftUIアプリでのサブスクリプション機能の基本的な実装を示しており、StoreKit 2の新しいAPI(async/await)を活用しています。


DownnLoad



参考