Swift | StoreKit2で自動更新のサブスク課金を実装
StoreKit 2は、iOSアプリ内での購買(特にサブスクリプション)の管理を簡素化するために導入されたフレームワークで、Appleが提供しています。Auto-Renewable Subscription(自動更新型サブスクリプション)は、一定の期間が経過すると自動的に更新されるサブスクリプションタイプで、アプリ内でよく利用されます。
自動更新のサブスク課金を実装するコード
以下のファイルは、StoreKit 2を使用したサブスクリプション機能の実装に必要なファイルを示しています。
各ファイルの主な機能を説明します:
StoreKit2App.swift
メインアプリケーションのエントリーポイント。
ContentViewをルートビューとして設定しています。
// StoreKit2App.swift
import SwiftUI
@main
struct StoreKit2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
ContentView.swift
アプリのメインビュー。
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
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
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
参考