In-App Purchases and Subscriptions with StoreKit 2
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
FieldNotes is free, with an optional subscription, FieldNotes Pro, that unlocks unlimited notebooks, the
augmented-reality map on Augmented Reality with ARKit and RealityKit
and end-to-end encrypted note sharing from Security and Privacy.
This page builds the store with StoreKit 2, the Swift-concurrency API that replaced the original
SKPaymentQueue API, using the 27 releases (iOS, iPadOS, macOS, watchOS and visionOS 27), Xcode 27 and
Swift 6.4, with the app’s deployment minimum of 26. The monthly-with-12-month-commitment billing plan
needs 26.4 and the new offer-code redemption API needs 27, so both are guarded with if #available.
The page adds one type to FieldNotesServices: StoreManager, an @Observable @MainActor class that loads
the two Pro products, runs purchases, listens for transactions from launch, and publishes whether the person is
entitled to Pro. A StoreKit configuration file, Products.storekit, lets all of it run in Xcode without App
Store Connect.
Products and What FieldNotes Sells
App Store Connect offers four product types:
| Type | Behaviour | Example |
|---|---|---|
Consumable |
Used up, can be bought again; not restored |
Extra AI summaries (FieldNotes does not sell these) |
Non-consumable |
Bought once, owned forever, restored on every device |
A one-off "lifetime Pro" unlock |
Auto-renewable subscription |
Renews every period until cancelled; belongs to a subscription group |
FieldNotes Pro, monthly or yearly |
Non-renewing subscription |
Time-limited access that does not renew; the app tracks expiry |
A season pass |
FieldNotes Pro is one subscription group, FieldNotes Pro, with two products. A person can hold only one product of a group at a time, and moving between them is an upgrade, downgrade or crossgrade that the App Store handles.
| Product ID | Period | Notes |
|---|---|---|
|
1 month |
Billed monthly, cancel any time |
|
1 year |
Billed up front, or (26.4 and later) monthly with a 12-month commitment on the same product ID |
Platform availability
| API or feature | iOS | iPadOS | macOS | watchOS | visionOS |
|---|---|---|---|---|---|
|
Yes |
Yes |
Yes |
Yes |
Yes |
|
Yes |
Yes |
Yes |
Yes |
Not available (use |
|
Yes |
Yes |
Yes |
Yes |
Yes |
|
Yes |
Yes |
Yes |
Yes |
Yes |
|
Yes |
Yes |
Not available (use the App Store app) |
Not available |
Yes |
12-month commitment billing plan ( |
New in 26.4 |
New in 26.4 |
New in 26.4 |
New in 26.4 |
New in 26.4 |
|
New in 27 |
New in 27 |
New in 27 |
Not available |
New in 27 |
StoreKit testing in Xcode ( |
Yes |
Yes |
Yes |
Yes |
Yes |
Every example on this page needs a paid Apple Developer Program membership and a signed Paid Apps agreement in
App Store Connect to sell real products; the local Products.storekit configuration described in
StoreKit Configuration Files and Testing in Xcode needs neither.
How a Purchase Flows
In StoreKit 2 the App Store signs every transaction as a JWS (JSON Web Signature, the same format as a JWT,
see JWT and JOSE). StoreKit verifies the signature on the device before
the app sees it and hands the app a VerificationResult<Transaction>: .verified or .unverified. There is no
receipt to parse. A purchase can complete in the app, but it can also complete elsewhere: after Ask to Buy
approval, on another device, as a renewal at 3 a.m., or as a refund. All of those arrive through
Transaction.updates, which is why the listener has to run from launch.
Figure: purchase to unlock
StoreManager
StoreManager lives in FieldNotesServices so every target, including the widget, can ask whether Pro is
unlocked. The FieldNotesApp scene owns one instance for the app’s lifetime.
// Packages/FieldNotesKit/Sources/FieldNotesServices/Store/StoreManager.swift
import Foundation
import Observation
import StoreKit
@Observable @MainActor
public final class StoreManager {
public static let productIDs: [Product.ID] = [
"com.example.fieldnotes.pro.monthly",
"com.example.fieldnotes.pro.yearly",
]
/// The "FieldNotes Pro" group ID from App Store Connect (and from Products.storekit).
public static let proGroupID = "21500001"
public private(set) var products: [Product] = []
public private(set) var isPro = false
public private(set) var proRenewalState: Product.SubscriptionInfo.RenewalState?
@ObservationIgnored private var updatesTask: Task<Void, Never>?
public init() {
// Start listening before anything else, so no transaction is missed.
updatesTask = Task { [weak self] in
for await result in Transaction.updates {
await self?.handle(result)
}
}
}
public func loadProducts() async {
do {
products = try await Product.products(for: Self.productIDs)
.sorted { $0.price < $1.price }
} catch {
products = [] // offline or misconfigured: the store UI shows a retry button
}
}
}
See Apple Developer Documentation — products(for:) and Apple Developer Documentation — Implementing a store in your app using the StoreKit API.
Product.products(for:) returns only the IDs the App Store knows for the current storefront, localized, with
displayName, description, displayPrice (already formatted in the person’s currency) and, for
subscriptions, a subscription property with the period, group and offers. Never hard-code prices.
The Task created in init inherits the main actor, so handle(_:) runs on the main actor and can update
isPro directly. The [weak self] capture keeps the loop from retaining the manager; in FieldNotes the manager
lives as long as the app, so the task never ends. How tasks inherit isolation is covered on
Async/Await and Tasks and
Concurrency in Apps.
The app creates it at launch and injects it next to the note store:
// FieldNotes/FieldNotesApp.swift (excerpt)
import FieldNotesServices
import SwiftUI
@main
struct FieldNotesApp: App {
@State private var store = AppServices.shared.store // the single owner, see Background Execution
@State private var storeManager = StoreManager() // Transaction.updates starts here
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
.environment(storeManager)
.task {
await storeManager.loadProducts()
await storeManager.refreshEntitlements()
}
}
}
}
Purchasing
A purchase needs a UI context to anchor the payment sheet. Product.purchase(options:) finds one on its own
on iOS, macOS and watchOS but does not exist on visionOS, and the purchase(confirmIn:options:) variants take a
UIKit scene or an AppKit window. The portable choice in SwiftUI is PurchaseAction, read from the environment
as @Environment(\.purchase). StoreManager accepts it as a parameter, so the manager stays free of UI code:
// StoreManager.swift (continued)
extension StoreManager {
public enum PurchaseOutcome { case unlocked, pending, cancelled }
public func purchase(_ product: Product, using purchaseAction: PurchaseAction,
accountID: UUID? = nil) async throws -> PurchaseOutcome {
var options: Set<Product.PurchaseOption> = []
if let accountID { options.insert(.appAccountToken(accountID)) } // ties it to the FieldNotes account
switch try await purchaseAction(product, options: options) {
case .success(let verification):
await handle(verification)
return isPro ? .unlocked : .pending
case .pending:
return .pending // Ask to Buy or Strong Customer Authentication; arrives via updates
case .userCancelled:
return .cancelled
@unknown default:
return .cancelled
}
}
}
// FieldNotes/Views/ProUpgradeButton.swift
struct ProUpgradeButton: View {
@Environment(StoreManager.self) private var storeManager
@Environment(\.purchase) private var purchase
let product: Product
var body: some View {
Button("Subscribe for \(product.displayPrice)") {
Task { _ = try? await storeManager.purchase(product, using: purchase) }
}
}
}
See Apple Developer Documentation — PurchaseAction, Apple Developer Documentation — purchase(options:) and Apple Developer Documentation — Product.PurchaseResult.
.appAccountToken(_:) stores a UUID of the app’s choosing in the signed transaction. FieldNotes passes the
account ID from AccountManager, so its server can match App Store Server Notifications to the right account.
.pending is not a failure: the transaction may still succeed hours later and will arrive through
Transaction.updates.
Verifying and Finishing Transactions
handle(_:) is the single place where a transaction changes what the person can do. It is called from the
purchase result and from the updates listener.
// StoreManager.swift (continued)
extension StoreManager {
func handle(_ result: VerificationResult<Transaction>) async {
switch result {
case .verified(let transaction):
await refreshEntitlements()
await transaction.finish() // tell the App Store the content was delivered
case .unverified(let transaction, let error):
// Signed by someone else, or for another device: never unlock anything.
print("Unverified transaction \(transaction.id): \(error)")
}
}
}
See Apple Developer Documentation — Transaction, Apple Developer Documentation — VerificationResult and Apple Developer Documentation — finish().
Only finish a transaction after the app has delivered what was bought. An unfinished transaction is delivered
again through Transaction.updates at the next launch, which is the retry mechanism. For consumables, finishing
is essential: until it happens the same product cannot be bought again.
On-device verification protects against a tampered app or a replayed transaction on another device, but a
jailbroken device can still lie to its own app. When Pro unlocks a server feature, such as sharing through the
FieldNotes server, the server must also check the purchase with the App Store Server API or App Store Server
Notifications V2; the VerificationResult also exposes the raw jwsRepresentation for that purpose.
Entitlements and Subscription Status
Transaction.currentEntitlements yields the latest transaction for every product the person is currently
entitled to: active subscriptions, owned non-consumables, including purchases made on other devices and through
Family Sharing. It is the source of truth for isPro:
// StoreManager.swift (continued)
extension StoreManager {
public func refreshEntitlements() async {
var entitled = false
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result,
Self.productIDs.contains(transaction.productID),
transaction.revocationDate == nil, // not refunded
!transaction.isUpgraded else { continue } // superseded by another product
if let expiry = transaction.expirationDate, expiry < .now { continue }
entitled = true
}
isPro = entitled
}
}
See Apple Developer Documentation — currentEntitlements, Apple Developer Documentation — revocationDate and Apple Developer Documentation — isUpgraded.
isPro is published through Observation, so every view that reads it re-renders when a renewal or refund
arrives. To check a single product, Transaction.currentEntitlements(for:) (18.4 and later, so always
available at FieldNotes’s minimum) filters by product ID.
Entitlement answers whether Pro is active. Subscription status answers how: subscribed, in a billing grace period, in billing retry, expired or revoked. FieldNotes uses it to show a "Payment problem, update your payment method" banner while a subscription is in grace period, instead of silently locking the person out:
// StoreManager.swift (continued)
extension StoreManager {
public func refreshStatus() async {
let statuses = (try? await Product.SubscriptionInfo.status(for: Self.proGroupID)) ?? []
proRenewalState = statuses
.filter { if case .verified = $0.transaction { true } else { false } }
.map(\.state)
.first
}
}
// In a view, the modifier form keeps the status current without a manual refresh:
// .subscriptionStatusTask(for: StoreManager.proGroupID) { taskState in
// if case .success(let statuses) = taskState,
// statuses.contains(where: { $0.state == .inGracePeriod }) { showPaymentBanner = true }
// }
See Apple Developer Documentation — Product.SubscriptionInfo.Status, Apple Developer Documentation — Product.SubscriptionInfo.RenewalState and Apple Developer Documentation — subscriptionStatusTask(for:priority:action:).
A person with Family Sharing may have more than one status for the same group (their own and a family member’s); FieldNotes takes the first verified one, which is enough for a banner.
Restoring Purchases with AppStore.sync()
Because currentEntitlements already includes purchases made on other devices, most people never need to
restore anything. App Review still requires a visible Restore Purchases control for non-consumables and
subscriptions, for the rare case where the device’s transaction data is out of date. AppStore.sync() forces a
fresh sync with the App Store and asks the person to authenticate, so call it only from that explicit button:
// FieldNotes/Views/SettingsView+Store.swift (excerpt)
Button("Restore Purchases") {
Task {
try? await AppStore.sync()
await storeManager.refreshEntitlements()
}
}
Merchandising with StoreKit Views
StoreKit’s SwiftUI views load products, format prices, show the right legal text and buttons for each platform, and run the purchase. They are the fastest way to a store that passes review.
SubscriptionStoreView for FieldNotes Pro
SubscriptionStoreView(groupID:) shows every product in a subscription group, with custom marketing content on
top. The .storeButton(_:for:) modifier adds the restore and redeem-code buttons, and
.onInAppPurchaseCompletion routes the result to StoreManager:
// FieldNotes/Views/ProStoreView.swift
import FieldNotesServices
import StoreKit
import SwiftUI
struct ProStoreView: View {
@Environment(StoreManager.self) private var storeManager
@Environment(\.dismiss) private var dismiss
var body: some View {
SubscriptionStoreView(groupID: StoreManager.proGroupID) {
VStack(spacing: 12) {
Image(systemName: "note.text.badge.plus")
.font(.system(size: 56))
.foregroundStyle(.tint)
Text("FieldNotes Pro").font(.largeTitle.bold())
Text("Unlimited notebooks, the AR notes map and encrypted sharing.")
.multilineTextAlignment(.center)
}
.padding()
}
#if os(watchOS)
.storeButton(.visible, for: .restorePurchases)
#else
.subscriptionStoreControlStyle(.picker)
.storeButton(.visible, for: .restorePurchases, .redeemCode)
#endif
.onInAppPurchaseCompletion { _, result in
if case .success(.success(let verification)) = result {
await storeManager.handle(verification)
if storeManager.isPro { dismiss() }
}
}
}
}
#Preview {
ProStoreView()
.environment(StoreManager())
}
See Apple Developer Documentation — SubscriptionStoreView, Apple Developer Documentation -- storeButton(_:for:), Apple Developer Documentation -- subscriptionStoreControlStyle(_:) and Apple Developer Documentation — onInAppPurchaseCompletion(perform:).
.picker and the redeem-code button are not available on watchOS, hence the #if. The preview works without App Store Connect when the scheme’s StoreKit configuration is Products.storekit
(see StoreKit Configuration Files and Testing in Xcode). On visionOS the same view renders in a window with glass material; on watchOS it becomes a
scrolling list. The design guidance for paywalls, including what must be visible before someone subscribes, is
in Human Interface Guidelines — In-app purchase.
StoreView and ProductView
StoreView(ids:) merchandises any set of products, not only one subscription group, and ProductView(id:)
shows a single product inline, for example as a row in SettingsView:
// SettingsView (excerpt): an inline upsell row, hidden once Pro is active
if !storeManager.isPro {
ProductView(id: "com.example.fieldnotes.pro.yearly") {
Image(systemName: "star.circle.fill")
}
#if os(iOS) || os(macOS)
.productViewStyle(.compact) // not available on watchOS and visionOS
#endif
}
// A full store of every FieldNotes product
StoreView(ids: StoreManager.productIDs)
Managing the subscription
People manage and cancel subscriptions in the system’s subscription settings. On iOS, iPadOS and visionOS the app can open them in a sheet, scoped to the Pro group so the commitment progress described below is shown:
#if os(iOS) || os(visionOS)
struct ManageProButton: View {
@State private var isPresented = false
var body: some View {
Button("Manage Subscription") { isPresented = true }
.manageSubscriptionsSheet(isPresented: $isPresented,
subscriptionGroupID: StoreManager.proGroupID)
}
}
#endif
On macOS and watchOS the sheet is not available; link to the App Store’s subscription page instead.
Monthly Subscriptions with a 12-Month Commitment
The 2026 billing option lets people pay for a yearly subscription monthly, in exchange for committing to
twelve payments. It is not a new product: in App Store Connect you enable the monthly billing plan on an existing
1-year subscription, so com.example.fieldnotes.pro.yearly keeps its product ID and now offers two pricing
terms: billed up front, or billed monthly with a 12-month commitment. Cancelling stops the commitment from
renewing, but the remaining payments of the current commitment are still billed.
The APIs arrived in 26.4 and need an app built with the Xcode 26.5 SDK or later. The billing plan is offered
worldwide except in the United States and Singapore; in those storefronts pricingTerms contains only the
up-front entry. Because FieldNotes’s minimum is 26, every use is guarded:
// StoreManager.swift (continued)
extension StoreManager {
/// The yearly product's monthly commitment plan, if this storefront offers it.
@available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *)
public func commitmentTerms(for product: Product) -> Product.SubscriptionInfo.PricingTerms? {
product.subscription?.pricingTerms.first { $0.billingPlanType == .monthly }
}
@available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *)
public func purchaseCommitment(_ product: Product, using purchaseAction: PurchaseAction) async throws {
let result = try await purchaseAction(product, options: [.billingPlanType(.monthly)])
if case .success(let verification) = result { await handle(verification) }
}
}
// FieldNotes/Views/CommitmentOfferRow.swift
struct CommitmentOfferRow: View {
@Environment(StoreManager.self) private var storeManager
@Environment(\.purchase) private var purchase
let yearly: Product
var body: some View {
if #available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *),
let terms = storeManager.commitmentTerms(for: yearly) {
VStack(alignment: .leading) {
// Both amounts must be visible before the person subscribes.
Text("\(terms.billingDisplayPrice) per month for 12 months")
Text("Total commitment: \(terms.commitmentInfo.displayPrice)")
.font(.footnote).foregroundStyle(.secondary)
Button("Pay Monthly") {
Task { try? await storeManager.purchaseCommitment(yearly, using: purchase) }
}
}
}
}
}
See Apple Developer Documentation — Supporting monthly subscriptions with a 12-month commitment Apple Developer Documentation — Product.SubscriptionInfo.PricingTerms and WWDC26 — What’s new in Apple In-App Purchase.
With SubscriptionStoreView, a single modifier makes the store feature the commitment plan instead:
SubscriptionStoreView(groupID: StoreManager.proGroupID)
.preferredSubscriptionPricingTerms { _, subscriptionInfo in
subscriptionInfo.pricingTerms.first { $0.billingPlanType == .monthly }
}
In the app, each monthly payment is an ordinary transaction that grants one month; the entitlement logic in
refreshEntitlements() needs no change, because it already checks the transaction’s own expirationDate. The
transaction’s commitmentInfo (billingPeriodNumber, totalBillingPeriods, price, expirationDate) is only
for showing progress such as "payment 4 of 12". Never use commitmentInfo.expirationDate to decide access: a
billing issue that recovers late moves it. Billing grace period does not apply to commitment plans, so revoke
access while a monthly payment is in billing retry.
@available(iOS 26.4, macOS 26.4, watchOS 26.4, visionOS 26.4, *)
func commitmentProgress(of transaction: Transaction) -> String? {
guard transaction.billingPlanType == .monthly, let info = transaction.commitmentInfo else { return nil }
return "Payment \(info.billingPeriodNumber) of \(info.totalBillingPeriods)"
}
See Apple Developer Documentation — Transaction.CommitmentInfo and Apple Developer Documentation — Managing the life cycle of monthly subscriptions with a 12-month commitment.
The first time someone subscribes to a commitment plan, the system shows a one-time disclosure sheet with the
number of payments and cancellation terms; the app does nothing for it. On the server, the plan adds
billingPlanType and commitmentInfo fields to the decoded JWS transaction and renewal info, and the life-cycle
article lists which App Store Server Notifications (DID_RENEW, DID_CHANGE_RENEWAL_STATUS,
DID_FAIL_TO_RENEW, EXPIRED) to handle.
Offer Codes (27)
Offer codes give a free or discounted period, for example to a university field course. In 27 the SwiftUI
redemption sheet takes a set of RedeemOption values and returns the resulting transaction as a
VerificationResult, so the app can unlock immediately instead of waiting for Transaction.updates:
struct RedeemCodeButton: View {
@Environment(StoreManager.self) private var storeManager
@State private var isPresented = false
var body: some View {
Button("Redeem Code") { isPresented = true }
#if !os(watchOS)
.modifier(RedeemSheet(isPresented: $isPresented, storeManager: storeManager))
#endif
}
}
#if !os(watchOS)
private struct RedeemSheet: ViewModifier {
@Binding var isPresented: Bool
let storeManager: StoreManager
func body(content: Content) -> some View {
if #available(iOS 27, macOS 27, visionOS 27, *) {
content.offerCodeRedemption(options: [], isPresented: $isPresented) { result in
if case .success(let verification) = result {
Task { await storeManager.handle(verification) }
}
}
} else {
content.offerCodeRedemption(isPresented: $isPresented) // 26: the result arrives via updates
}
}
}
#endif
See Apple Developer Documentation — offerCodeRedemption(options:isPresented:onCompletion:), Apple Developer Documentation — RedeemOption and Apple Developer Documentation — Supporting offer codes in your app.
RedeemSheet is a page-local helper. Xcode 27 can also test offer-code redemption locally.
StoreKit Configuration Files and Testing in Xcode
A StoreKit configuration file describes products, subscription groups, prices and offers locally. When a
scheme’s Run > Options > StoreKit Configuration points at it, StoreKit talks to Xcode’s local test environment
instead of the App Store: no network, no sandbox account, instant renewals, and a Transaction Manager
(Debug > StoreKit > Manage Transactions) to approve Ask to Buy, refund, expire or fail renewals. Create one
with File > New > File from Template > StoreKit Configuration File, name it Products.storekit, and either
add the products by hand or sync them from App Store Connect.
The file is JSON that Xcode’s editor maintains; edit it in the editor, not by hand. An abbreviated excerpt shows its shape:
{
"subscriptionGroups" : [
{
"id" : "21500001",
"name" : "FieldNotes Pro",
"subscriptions" : [
{
"productID" : "com.example.fieldnotes.pro.monthly",
"displayPrice" : "3.99",
"recurringSubscriptionPeriod" : "P1M",
"groupNumber" : 2,
"type" : "RecurringSubscription",
"localizations" : [ { "locale" : "en_US", "displayName" : "FieldNotes Pro Monthly",
"description" : "All Pro features, billed monthly" } ]
},
{
"productID" : "com.example.fieldnotes.pro.yearly",
"displayPrice" : "34.99",
"recurringSubscriptionPeriod" : "P1Y",
"groupNumber" : 1,
"type" : "RecurringSubscription",
"localizations" : [ { "locale" : "en_US", "displayName" : "FieldNotes Pro Yearly",
"description" : "All Pro features, billed yearly" } ]
}
]
}
]
}
Since Xcode 26.5, the editor’s Billing Plan picker adds the monthly-with-12-month-commitment plan to the yearly
product, and the Transaction inspector shows the resulting commitmentInfo. Editor > Subscription Renewal
Rate speeds renewals up (for example, a monthly renewal every 30 seconds) to test expiry and win-back offers.
Unit tests drive the same file through SKTestSession from the StoreKitTest framework. Add
Products.storekit to the test target’s resources, then:
// FieldNotesKitTests/StoreManagerTests.swift
import FieldNotesServices
import StoreKit
import StoreKitTest
import Testing
@MainActor
struct StoreManagerTests {
@Test func buyingYearlyUnlocksPro() async throws {
let session = try SKTestSession(configurationFileNamed: "Products")
session.disableDialogs = true
session.clearTransactions()
let manager = StoreManager()
_ = try await session.buyProduct(identifier: "com.example.fieldnotes.pro.yearly", options: [])
await manager.refreshEntitlements()
#expect(manager.isPro)
try session.expireSubscription(productIdentifier: "com.example.fieldnotes.pro.yearly")
await manager.refreshEntitlements()
#expect(!manager.isPro)
}
}
Swift Testing itself is covered on Unit Testing with Swift Testing and Swift Testing.
The three test environments build on each other:
| Environment | Needs | Use it for |
|---|---|---|
Xcode ( |
Nothing: no account, no network |
Daily development, previews, unit and UI tests, edge cases via the Transaction Manager |
Sandbox |
App Store Connect products, a Sandbox Apple Account, a paid developer account |
Real signed transactions, the server’s App Store Server API and Notifications V2 handling |
TestFlight |
A TestFlight build |
Beta testers buying with accelerated renewals, never charged |
App Review and Payment Rules
The App Review Guidelines' section 3.1 governs payments. The rules FieldNotes has to follow:
-
3.1.1: digital content and features used in the app, such as FieldNotes Pro, must be sold through In-App Purchase. Physical goods and person-to-person services use other payment methods.
-
3.1.2: auto-renewable subscriptions must provide ongoing value, work on all the app’s supported devices, and clearly state what the person gets, the price, the period and, for commitment plans, the total commitment before purchase. Terms of use and a privacy policy link must be reachable from the paywall.
-
Restore: a way to restore purchases must exist (
AppStore.sync()or the.restorePurchasesstore button). -
Regional exceptions: some storefronts allow links to, or alternative, external payment. StoreKit’s External Purchase APIs implement those programmes and require an entitlement per region; FieldNotes does not use them.
In-App Purchases are submitted for review with the app. In 2026 App Store Connect groups products, in-app events
and custom product pages into one review submission, and the App Store Connect API’s reviewSubmissions
resources cover In-App Purchases too. Submission, metadata and TestFlight are covered on
App Store Connect, TestFlight and App Review.
References
-
Apple Developer Documentation — Implementing a store in your app using the StoreKit API
-
Apple Developer Documentation — Product.SubscriptionInfo.Status
-
Apple Developer Documentation — Product.SubscriptionInfo.RenewalState
-
Apple Developer Documentation — subscriptionStatusTask(for:priority:action:)
-
Apple Developer Documentation -- subscriptionStoreControlStyle(_:)
-
Apple Developer Documentation — onInAppPurchaseCompletion(perform:)
-
Apple Developer Documentation — manageSubscriptionsSheet(isPresented:subscriptionGroupID:)
-
Apple Developer Documentation — Supporting monthly subscriptions with a 12-month commitment
-
Apple Developer Documentation — Product.SubscriptionInfo.PricingTerms
-
Apple Developer Documentation -- preferredSubscriptionPricingTerms(_:)
-
Apple Developer Documentation — offerCodeRedemption(options:isPresented:onCompletion:)
-
Apple Developer Documentation — Supporting offer codes in your app
-
Apple Developer Documentation — Testing Apple In-App Purchases in Xcode
-
Apple Developer Documentation — Testing at all stages of development with Xcode and the sandbox