• StoreKit 2 및 Xcode 내 StoreKit Testing의 새로운 기능

    StoreKit 2의 최신 개선 사항과 Xcode의 StoreKit Testing에 대해 알아보세요. 앱 내 구입 홍보, StoreKit 메시지, 트랜잭션 모델, RenewalInfo 모델, 구독 관리용 App Store 시트에 대한 API 업데이트를 함께 살펴봅니다. 기기 내에서의 영수증 검증을 위해 SHA-256로 업그레이드하는 방법과 API를 사용하여 SwiftUI 뷰를 생성하는 방법도 알아보세요. 또한 앱 내 구입 및 구독을 디버깅하고 테스트할 수 있도록 Xcode에서 StoreKit Testing을 시작하는 방법도 알려드립니다. 트랜잭션 인스펙터를 알아보고, StoreKit 구성 에디터의 최신 업데이트 내용을 살펴보고, StoreKit 오류를 시뮬레이션하여 앱의 오류 처리를 테스트하는 방법을 배워보세요.

    리소스

    관련 비디오

    WWDC23

    WWDC22

    WWDC21

  • 다운로드
    Array
    • 1:42 - Create a listener for promoted in-app purchases

      // Create a listener for promoted in-app purchases
      import StoreKit
      
      let promotedPurchasesListener = Task {
          for await promotion in PurchaseIntent.intents {
              // Process promotion
              let product = promotion.product
      
              // Purchase promoted product
              do {
                  let result = try await product.purchase()
                  // Process result
              }
              catch {
                  // Handle error
              }
          }
      }
    • 2:57 - Check promotion order

      // Check promotion order
      import StoreKit
      
      do {
          let promotions = try await Product.PromotionInfo.currentOrder
      
          if promotions.isEmpty {
              // No local promotion order set
          }
      
          for promotion in promotions {
              let productID = promotion.productID
              let productVisibility = promotion.visibility
              // Check promoted products
          }
      }
      catch {
          // Handle error
      }
    • 3:26 - Set a promotion order

      // Set a promotion order
      import StoreKit
      
      let newPromotionOrder: [String] = [
          "acorns.individual",
          "nectar.cup",
          "sunflowerseeds.pile"
      ]
      
      do {
          try await Product.PromotionInfo.updateProductOrder(byID: newPromotionOrder)
      }
      catch {
          // Handle error
      }
    • 4:02 - Update promotion visibility

      // Update promotion visibility
      import StoreKit
      
      // Hide “acorns.individual”
      do {
          try await Product.PromotionInfo.updateProductVisibility(.hidden, for: "acorns.individual")
      }
      catch {
          // Handle error
      }
    • 4:17 - Update promotion visibility (alternative method)

      // Update promotion visibility
      import StoreKit
      
      do {
        let promotions = try await Product.PromotionInfo.currentOrder
      
        // Hide the first product
        if var firstPromotion = promotions.first {
          firstPromotion.visibility = .hidden
          try await firstPromotion.update()
        }
      }
      catch {
        // Handle error
      }
    • 8:32 - Product view

      // Product view
      import SwiftUI
      import StoreKit
      
      struct BirdFoodShop: View {
          let productID: String
          let productImage: String
      
          var body: some View {
              ProductView(id: productID) {
                  BirdFoodProductIcon(for: productID)
              }
              .productViewStyle(.large)
          }
      }
    • 8:52 - Store view

      // Store view
      import SwiftUI
      import StoreKit
      
      struct BirdFoodShop: View {
          let productIDs: [String]
      
          var body: some View {
              StoreView(ids: productIDs) { product in
                  BirdFoodIcon(productID: product.id)
              }
          }
      }
    • 9:19 - Subscription view

      // Subscription view
      import SwiftUI
      import StoreKit
      
      struct BackyardBirdsPassShop: View {
          let groupID: String
      
          var body: some View {
              SubscriptionStoreView(groupID: groupID)
          }
      }
    • 21:09 - Simulated off-device purchase using StoreKitTest

      // Simulated off-device purchase using StoreKitTest
      import StoreKit
      import StoreKitTest
      
      func testSubscriptionRenewal() async throws {
          let session = try SKTestSession(configurationFileNamed: "Store")
      
          let oneYearInterval: TimeInterval = (365 * 24 * 60 * 60)
          let transaction = try await session.buyProduct(
              identifier: "birdpass.individual",
              options: [
                  .purchaseDate(Date.now - oneYearInterval)
              ]
          )
      
          // Inspect transaction
      }
    • 21:48 - Set a simulated purchase error when loading products

      // Set a simulated purchase error when loading products
      import StoreKit
      import StoreKitTest
      
      func testLoadProducts() async throws {
          let session = try SKTestSession(configurationFileNamed: "Store")
          let productIDs = [
              "acorns.individual",
              "nectar.cup"
          ]
      
          // Set a simulated error, then load products, expecting an error
          session.setSimulatedError(.generic(.networkError), forAPI: .loadProducts)
          do {
              _ = try await Product.products(for: productIDs)
              XCTFail("Expected a network error")
          }
          catch StoreKitError.networkError(_) {
              // Expected error thrown, continue...
          }
          // Disable simulated error
          session.setSimulatedError(nil, forAPI: .loadProducts)
      }
    • 22:24 - Set a faster subscription renewal rate in a test session

      // Set a faster subscription renewal rate in a test session
      import StoreKit
      import StoreKitTest
      
      func testSubscriptionRenewal() async throws {
          let session = try SKTestSession(configurationFileNamed: "Store")
      
          // Set renewals to expire every minute
          session.timeRate = .oneRenewalEveryMinute
      
          let transaction = try await session.buyProduct(identifier: "birdpass.individual")
      
          // Wait for renewals and inspect transactions
      }