→ StoreKit

[StoreKit] 인앱결제 구현하기 2 - 코드

Swift librarian 2024. 2. 13. 01:21

이전 글

 

[StoreKit] 인앱결제 구현하기 1 - 기본 세팅

StoreKit 애플에서 인앱결제를 지원해주는 StoreKit 이 있다. 하지만 실제 서비스를 하는데에는 30% 라는 무시무시한 수수료를 감당해야 하기 때문에 비추하긴 하지만 간단한 결제를 쉽게 구현하기에

swift-library.tistory.com

간단하게 기본 세팅을 완료하였다. 이제 코드를 작성해볼 시간!! +_+

 

1. StoreKitManager 파일 및 Class 추가

나는 StoreKitManager 라는 이름으로 파일을 만들었다. 이렇게 내가 만든 Product 를 타입으로 지정할 수 있다.

import StoreKit

class StoreKitManager: ObservableObject {
    @Published var products: [Product] = []
}

 

2. requestProducts 함수 추가

우선 아래와 같이 products 들을 가져와야 한다. 그래야 뷰에 표시를 할 수 있으니까! @MainActor 를 통해 UI 업데이트와 같은 작업이 안전하게 수행될 수 있게 한다.

import StoreKit

class StoreKitManager: ObservableObject {
    @Published var products: [Product] = []
    
    private var productIDs = ["premium", "coffee"]
    
    init() {
        Task {
            await requestProducts()
        }
    }
    
    @MainActor
    func requestProducts() async {
        do {
            products = try await Product.products(for: productIDs)
        } catch {
            print("Failed to retrieving products \(error)")
        }
    }
}

 

3. 뷰 만들기

아래와 같이 StoreKitManager 를 불러와서 products 를 가져와서 displayName, displayPrice 를 가져올 수 있다. 안타깝게도 시뮬레이터에서만 확인할 수 있다. 프리뷰는 안된다!

import SwiftUI

struct InAppPurchaseView: View {
    @StateObject private var store = StoreKitManager()
    
    var body: some View {
        List {
            ForEach(store.products, id: \.id) { product in
                HStack {
                    Text(product.displayName)
                    Spacer()
                    Button(product.displayPrice) {
                        // purchasing action
                    }
                    .buttonStyle(.bordered)
                }
            }
        }
    }
}

 

4. Purchase 함수 구현

그렇다면 이제 Product 를 구입하는 함수를 만들어보자

    @Published var purchasedProducts: [Product] = [] //변수 추가

    @MainActor
    func purchase(_ product: Product) async throws -> Transaction? {
        let result = try await product.purchase()
        
        switch result {
        case .success(.verified(let transaction)):
            purchasedProducts.append(product)
            await transaction.finish()
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }

 

아래와 같이 뷰에 purchase 함수를 넣어주자

import SwiftUI

struct InAppPurchaseView: View {
    @StateObject private var store = StoreKitManager()
    
    var body: some View {
        List {
            ForEach(store.products, id: \.id) { product in
                HStack {
                    Text(product.displayName)
                    Spacer()
                    Button(product.displayPrice) {
                        Task {
                            try await store.purchase(product)
                        }
                    }
                    .buttonStyle(.bordered)
                }
            }
        }
    }
}

 

그렇다면 아래와 결제가 진행되는 과정을 볼 수 있다. 물론 테스트용이라 실제 돈이 나가지 않는다

 

5. Listening transaction 하기

사용자가 구매를 하게 된다면 이 상황을 계속해서 감지해줄 필요가 있다. 이것을 listen 이라고 하는데, 이것이 없어서 앱을 나갔다 다시 들어오게 된다면 앱 내에서 구매내역이 사라지게 된다.

 

아래의 함수를 추가해 주자.

    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                switch result {
                case let .verified(transaction):
                    
                    guard let product = self.products.first(where: { $0.id == transaction.productID } ) else { continue }
                    
                    self.purchasedProducts.append(product)
                    
                    await transaction.finish()
                default:
                    continue
                }
            }
        }
    }

 

하지만 이렇게 된다면 transaction이 검증되었다면 purchasedProducts 에 계속 product 를 추가하게 된다. 따라서 purchasedProductsSet<Product> 형식으로 만들어주고 self.purchasedProducts.append(product)self.purchasedProducts.insert(product) 로 만들어 주자.

 

전체 코드는 아래와 같다. init() 에도 listener 를 포함시켜 주었다.

import StoreKit

class StoreKitManager: ObservableObject {
    @Published var products: [Product] = []
    @Published var purchasedProducts: Set<Product> = []
    
    private var productIDs = ["premium", "coffee"]
    
    var transacitonListener: Task<Void, Error>?
    
    init() {
        transacitonListener = listenForTransactions()
        Task {
            await requestProducts()
        }
    }
    
    @MainActor
    func requestProducts() async {
        do {
            products = try await Product.products(for: productIDs)
        } catch {
            print("Failed to retrieving products \(error)")
        }
    }
    
    @MainActor
    func purchase(_ product: Product) async throws -> Transaction? {
        let result = try await product.purchase()
        
        switch result {
        case .success(.verified(let transaction)):
            purchasedProducts.insert(product)
            await transaction.finish()
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }
    
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                switch result {
                case let .verified(transaction):
                    
                    guard let product = self.products.first(where: { $0.id == transaction.productID } ) else { continue }
                    
                    self.purchasedProducts.insert(product)
                    
                    await transaction.finish()
                default:
                    continue
                }
            }
        }
    }
}

 

여기서 listenForTransactions() 를 아래와 같이 찢어준다.

    func listenForTransactions() -> Task<Void, Error> {
     return Task.detached {
      for await result in Transaction.updates {
       await self.handle(transactionVerification: result)
      }
     }
    }
    
    @MainActor
    private func handle(transactionVerification result: VerificationResult <Transaction> ) async {
      switch result {
        case let.verified(transaction):
          guard let product = self.products.first(where: { $0.id == transaction.productID } ) else { return }
          self.purchasedProducts.insert(product)
          await transaction.finish()
        default:
          return
      }
    }

 

6. 사용자가 구매한 상품 업데이트

현재 사용자가 구매한 상품을 업데이트해주는 함수도 추가해준다.

    private func updateCurrentEntitlements() async {
        for await result in Transaction.currentEntitlements {
            await self.handle(transactionVerification: result)
        }
    }

 

시작할때 products 를 가져온 후 실행되게 init() requestProducts() 아래 부분에 추가해준다.

    init() {
        transacitonListener = listenForTransactions()
        Task {
            await requestProducts()
            await updateCurrentEntitlements()
        }
    }

 

7. 만약 Consumable 한 상품을 구입했을때

배열로 저장하기 때문에 append 가 필요하다! 아래와 같이 addPurchased 를 

private func addPurchased(_ product: Product) {
  switch product.type {
   case .consumable:
     사용되는 구매항목.append(product)
   case .nonConsumable:
     지속되는 구매항목.insert(product)
   default:
   return
  }
}

 

handle 함수의 아래 부분을 대체해주면 된다.

self.purchasedProducts.insert(product)

self.addPurchased(product)

 

코드는 거의 다 완성되었다! 다음은 App Store Connect 와 연결하고 간단한 작업만 하면 된다!