View in English

  • Apple Developer
    • Get Started

    Explore Get Started

    • Overview
    • Learn
    • Apple Developer Program

    Stay Updated

    • Latest News
    • Hello Developer
    • Platforms

    Explore Platforms

    • Apple Platforms
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    Featured

    • Design
    • Distribution
    • Games
    • Accessories
    • Web
    • Home
    • CarPlay
    • Technologies

    Explore Technologies

    • Overview
    • Xcode
    • Swift
    • SwiftUI

    Featured

    • Accessibility
    • App Intents
    • Apple Intelligence
    • Games
    • Machine Learning & AI
    • Security
    • Xcode Cloud
    • Community

    Explore Community

    • Overview
    • Meet with Apple events
    • Community-driven events
    • Developer Forums
    • Open Source

    Featured

    • WWDC
    • Swift Student Challenge
    • Developer Stories
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Centers
    • Documentation

    Explore Documentation

    • Documentation Library
    • Technology Overviews
    • Sample Code
    • Human Interface Guidelines
    • Videos

    Release Notes

    • Featured Updates
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • Downloads

    Explore Downloads

    • All Downloads
    • Operating Systems
    • Applications
    • Design Resources

    Featured

    • Xcode
    • TestFlight
    • Fonts
    • SF Symbols
    • Icon Composer
    • Support

    Explore Support

    • Overview
    • Help Guides
    • Developer Forums
    • Feedback Assistant
    • Contact Us

    Featured

    • Account Help
    • App Review Guidelines
    • App Store Connect Help
    • Upcoming Requirements
    • Agreements and Guidelines
    • System Status
  • Quick Links

    • Events
    • News
    • Forums
    • Sample Code
    • Videos
 

Videos

Abrir menú Cerrar menú
  • Colecciones
  • Todos los videos
  • Información

Más videos

  • Información
  • Código
  • What's new in StoreKit testing

    Discover the latest tools to help you test your in-app purchases and subscriptions. We'll show you how to bring your products from App Store Connect into StoreKit Testing in Xcode, learn about improvements to the transaction manager, and explore your in-app purchase flow in Xcode Previews. We'll also take you through best practices when setting up an Apple ID for the sandbox environment, and show you how to create tests for refund requests, price increase consent, billing retry, and much more.

    Recursos

    • Implementing offer codes in your app
    • Learn more about setting up offer codes
    • Reducing Involuntary Subscriber Churn
    • Testing In-App Purchases with sandbox
    • Setting up StoreKit Testing in Xcode
    • Handling Subscriptions Billing
    • Auto-renewable subscriptions overview
      • Video HD
      • Video SD

    Videos relacionados

    WWDC23

    • Explore testing in-app purchases
    • Meet StoreKit for SwiftUI
    • What’s new in StoreKit 2 and StoreKit Testing in Xcode

    WWDC22

    • What's new with in-app purchase
    • What's new with SKAdNetwork

    WWDC21

    • Manage in-app purchases on your server
    • Meet StoreKit 2

    WWDC20

    • Introducing StoreKit Testing in Xcode
  • Buscar este video…
    • 6:58 - Subscription option view

      VStack(alignment: .leading) {
          Text(subscription.displayName)
              .font(.headline.weight(.semibold))
          Text(subscription.description)
      }
    • 11:18 - Refund view

      struct RefundView: View {
          @State private var selectedTransactionID: UInt64?
          @State private var refundSheetIsPresented = false
          @Environment(\.dismiss) private var dismiss
          var body: some View {
              Button {
                  refundSheetIsPresented = true
              } label: {
                  Text("Request a refund")
                      .bold()
                      .padding(.vertical, 5)
                      .frame(maxWidth: .infinity)
              }
              .buttonStyle(.borderedProminent)
              .padding([.horizontal, .bottom])
              .disabled(selectedTransactionID == nil)
              .refundRequestSheet(
                  for: selectedTransactionID ?? 0,
                  isPresented: $refundSheetIsPresented
              ) { result in
                  if case .success(.success) = result {
                      dismiss()
                  }
              }
          }
      }
    • 12:33 - Refunds emit an updated value from the transaction updates sequence

      for await update in Transaction.updates {
          let transaction = try update.payloadValue
        
          if let revocationDate = transaction.revocationDate,
        	   let revocationReason = transaction.revocationReason {
              print("\(transaction.productID) revoked on \(revocationDate)")
             
              switch revocationReason {
              case .developerIssue: <#Handle developer issue#>
              case .other: <#Handle other issue#>
              default: <#Handle unknown reason#>
              }
              
              <#Revoke access to the product#>
          }
          <#...#>
      }
    • 14:21 - Offer code view

      struct SubscriptionPurchaseView: View {
          @State private var redeemSheetIsPresented = false
              
          var body: some View {
              Button("Redeem an offer") {
                  redeemSheetIsPresented = true
              }
              .buttonStyle(.borderless)
              .frame(maxWidth: .infinity)
              .padding(.vertical)
              .offerCodeRedeemSheet(isPresented: $redeemSheetIsPresented)
          }
      
      }
    • 16:23 - Offer redemptions emit updated values from Transaction.updates and Product.SubscriptionInfo.Status.updates

      for await verificationResult in Transaction.updates {
          guard case .verified(let transaction) = verificationResult else {
              <#Handle failed verification#>
          }
          <#Handle updated transaction#>
      }
      
      for await updatedStatus in Product.SubscriptionInfo.Status.updates {
          guard case .verified(let renewalInfo) = updatedStatus.renewalInfo else {
              <#Handle failed verification#>
          }
          <#Handle updated status#>
      }
    • 16:31 - Check the active offer on the transaction value

      for await status in Product.SubscriptionInfo.Status.updates {
          let transaction = try status.transaction.payloadValue
          let renewalInfo = try status.renewalInfo.payloadValue
          
          if let currentOfferType = transaction.offerType {
              switch currentType {
              case .introductory: <#Handle introductory offer#>
              case .promotional:  <#Handle promotional offer#>
              case .code:         <#Handle offer for codes#>
              default:            <#Handle unknown offer type#>
              }
              self.hasCurrentOffer = true
          }
      
          <#...#>
      
      }
    • 16:49 - Check the next pending offer on the renewal info value

      for await status in Product.SubscriptionInfo.Status.updates {
          let transaction = try status.transaction.payloadValue
          let renewalInfo = try status.renewalInfo.payloadValue
          
          <#Check active current offer#>
          
          if let nextOfferType = renewalInfo.offerType {
              switch currentType {
              case .introductory: <#Handle introductory offer#>
              case .promotional: <#Handle promotional offer#>
              case .code:
                  print("Customer has \(renewalInfo.offerID) queued")
                  <#Handle offer for codes#>
              default: <#Handle unknown offer type#>
              }
              self.hasQueuedOffer = true
          }
          <#...#>
      }
    • 18:45 - Messages updates loop

      private var pendingMessages: [Message] = []
      
      private func updatesLoop() {
          for await message in Message.messages {
            if <#Check if sensitive view is presented#>,
               let display: DisplayMessageAction = <#Get display message action#> {
                 try? display(message)
            }
            else {
              pendingMessages.append(message)
            }
          }
      }
    • 20:53 - Price increase changes emit an updated value from the status updates sequence

      for await status in Product.SubscriptionInfo.Status.updates {
          let renewalInfo = try status.renewalInfo.payloadValue
      
          if renewalInfo.priceIncreaseStatus == .agreed {
              print("Customer consented to price increase")
              <#Handle consented to price increase#>
          }
          if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
              print("Customer did not consent to price increase")
              <#Handle expired due to not consenting to price increase#>
          }
      
          <#...#>
      
      }
    • 21:19 - Unit testing price increases

      let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>")
      session.disableDialogs = true
      
      <#Purchase a subscription#>
      
      var transaction: SKTestTransaction! = session.allTransactions().first
      session.requestPriceIncreaseConsentForTransaction(identifier: transaction.identifier)
      
      transaction = session.allTransactions().first
      XCTAssertTrue(transaction.isPendingPriceIncreaseConsent)
      
      <#Assert app updates for pending price increase#>
      
      // Write a test case for consenting and cancelling due to price increase:
      
      session.consentToPriceIncreaseForTransaction(identifier: transaction.identifier)
      
      // OR
      
      session.declinePriceIncreaseForTransaction(identifier: transaction.identifier)
      session.expireSubscription(productIdentifier: "<#Product ID#>")
      
      <#Assert app updates for finished price increase#>
    • 24:57 - Billing retry and grace period status changes emit an updated value from the status updates sequence

      for await status in Product.SubscriptionInfo.Status.updates {
          let renewalInfo = try status.renewalInfo.payloadValue
      
          if let gracePeriodExpirationDate = renewalInfo.gracePeriodExpirationDate,
             gracePeriodExpirationDate < .now {
              print("In grace period until \(gracePeriodExpirationDate)”)
              <#Allow access to subscription#>
          }
          else if renewalInfo.isInBillingRetry {
              <#Handle billing retry#>
          }
      
          <#...#>
      
      }
    • 25:27 - Using the state property of a status value to check for billing retry states

      struct SubscriptionStatusView: View {
          let currentSubscription: Product
          let status: Product.SubscriptionInfo.Status
          @Environment(\.openURL) var openURL
          var body: some View {
              Section("Your Subscription") {
                  <#...#>
                  if status.state == .inBillingRetryPeriod || status.state == .inGracePeriod {
                      VStack {
                          Text("""
                          There was a problem renewing your subscription. Open the App Store to
                          update your payment information.
                          """)
                          Button("Open the App Store") {
                              openURL(URL(string: "https://apps.apple.com/account/billing")!)
                          }
                      }
                  }
              }
          }
      }
    • 25:41 - Current entitlement APIs will account for grace period

      for await entitlement in Transaction.currentEntitlements {
          <#Grant access to product#>
      }
    • 25:50 - Unit testing billing retry and grace period

      let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>")
      session.billingGracePeriodIsEnabled = true
      session.shouldEnterBillingRetryOnRenewal = true
      
      <#Purchase a subscription#>
      
      wait(for: [<#XCTExpectation#>], timeout: 60)
      
      let transaction: SKTestTransaction! = session.allTransactions().first
      XCTAssertTrue(transaction.hasPurchaseIssue)
      
      <#Assert app still allows access to subscription due to grace period#>
      
      wait(for: [<#XCTExpectation#>], timeout: 60)
      
      <#Assert app detects billing retry and no longer allows access to subscription#>
      
      session.resolveIssueForTransaction(identifier: transaction.identifier)
      
      <#Assert app allows access to subscription#>

Developer Footer

  • Videos
  • WWDC22
  • What's new in StoreKit testing
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • Apple Intelligence
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Downloads
    • Sample Code
    • Videos
    Open Menu Close Menu
    • Help Guides & Articles
    • Contact Us
    • Forums
    • Feedback & Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Read the latest news.
    Get the Apple Developer app.
    Copyright © 2026 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines