smithery.ai

ios-storekit

StoreKit expert for in-app purchases and subscriptions. Use when working with StoreKit 2, subscriptions, consumables, non-consumables, receipt validation, or App Store transactions.

First seen Mar 21, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,173 B
  • docs SUMMARY.md 201 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

iOS StoreKit

Expert guidance for implementing in-app purchases with StoreKit 2.

Setup

Configure Products in App Store Connect

  1. Create app in App Store Connect
  2. Add in-app purchases (Subscriptions, Consumables, Non-Consumables)
  3. Configure pricing and availability

StoreKit Configuration (Testing)

Create Configuration.storekit file for local testing.

Store Manager

import StoreKit

@MainActor
class StoreManager: ObservableObject {
    @Published var products: [Product] = []
    @Published var purchasedProductIDs: Set<String> = []
    @Published var isLoading = false

    private let productIDs: Set<String> = [
        "com.app.premium.monthly",
        "com.app.premium.yearly",
        "com.app.coins.100",
        "com.app.removeads"
    ]

    private var updateListenerTask: Task<Void, Error>?

    init() {
        updateListenerTask = listenForTransactions()
        Task {
            await loadProducts()
            await updatePurchasedProducts()
        }
    }

    deinit {
        updateListenerTask?.cancel()
    }
}

Loading Products

Fetch Products

func loadProducts() async {
    isLoading = true
    defer { isLoading = false }

    do {
        products = try await Product.products(for: productIDs)
            .sorted { $0.price < $1.price }
    } catch {
        print("Failed to load products: \(error)")
    }
}

Product Types

extension Product {
    var isSubscription: Bool {
        type == .autoRenewable
    }

    var isConsumable: Bool {
        type == .consumable
    }

    var isNonConsumable: Bool {
        type == .nonConsumable
    }
}

Purchases

Purchase Product

func purchase(_ product: Product) async throws -> Transaction? {
    let result = try await product.purchase()

    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        await updatePurchasedProducts()
        await transaction.finish()
        return transaction

    case .userCancelled:
        return nil

    case .pending:
        return nil

    @unknown default:
        return nil
    }
}

private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .unverified:
        throw StoreError.verificationFailed
    case .verified(let safe):
        return safe
    }
}

Transaction Listener

func listenForTransactions() -> Task<Void, Error> {
    Task.detached {
        for await result in Transaction.updates {
            do {
                let transaction = try await self.checkVerified(result)
                await self.updatePurchasedProducts()
                await transaction.finish()
            } catch {
                print("Transaction failed verification: \(error)")
            }
        }
    }
}

Subscriptions

Check Subscription Status

func updatePurchasedProducts() async {
    var purchased: Set<String> = []

    for await result in Transaction.currentEntitlements {
        do {
            let transaction = try checkVerified(result)

            if transaction.revocationDate == nil {
                purchased.insert(transaction.productID)
            }
        } catch {
            print("Failed to verify transaction: \(error)")
        }
    }

    await MainActor.run {
        self.purchasedProductIDs = purchased
    }
}

Subscription Info

func getSubscriptionStatus(for groupID: String) async throws -> Product.SubscriptionInfo.Status? {
    guard let product = products.first(where: { $0.subscription?.subscriptionGroupID == groupID }) else {
        return nil
    }

    return try await product.subscription?.status.first
}

func isSubscriptionActive(groupID: String) async -> Bool {
    do {
        guard let status = try await getSubscriptionStatus(for: groupID) else {
            return false
        }

        switch status.state {
        case .subscribed, .inGracePeriod:
            return true
        default:
            return false
        }
    } catch {
        return false
    }
}

Subscription Period Display

extension Product.SubscriptionPeriod {
    var displayName: String {
        switch unit {
        case .day:
            return value == 7 ? "Weekly" : "\(value) Day\(value > 1 ? "s" : "")"
        case .week:
            return "\(value) Week\(value > 1 ? "s" : "")"
        case .month:
            return value == 1 ? "Monthly" : "\(value) Months"
        case .year:
            return value == 1 ? "Yearly" : "\(value) Years"
        @unknown default:
            return "Unknown"
        }
    }
}

Restore Purchases

func restorePurchases() async throws {
    try await AppStore.sync()
    await updatePurchasedProducts()
}

SwiftUI Views

Product List View

struct StoreView: View {
    @StateObject private var store = StoreManager()

    var body: some View {
        List {
            Section("Subscriptions") {
                ForEach(store.products.filter { $0.isSubscription }) { product in
                    ProductRow(product: product, isPurchased: store.purchasedProductIDs.contains(product.id)) {
                        Task {
                            try await store.purchase(product)
                        }
                    }
                }
            }

            Section("One-Time Purchases") {
                ForEach(store.products.filter { !$0.isSubscription }) { product in
                    ProductRow(product: product, isPurchased: store.purchasedProductIDs.contains(product.id)) {
                        Task {
                            try await store.purchase(product)
                        }
                    }
                }
            }

            Section {
                Button("Restore Purchases") {
                    Task {
                        try await store.restorePurchases()
                    }
                }
            }
        }
    }
}

struct ProductRow: View {
    let product: Product
    let isPurchased: Bool
    let action: () -> Void

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.displayName)
                    .font(.headline)
                Text(product.description)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            if isPurchased {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundStyle(.green)
            } else {
                Button(product.displayPrice, action: action)
                    .buttonStyle(.borderedProminent)
            }
        }
    }
}

Subscription Store View (iOS 17+)

import StoreKit

struct PremiumView: View {
    var body: some View {
        SubscriptionStoreView(groupID: "your_group_id") {
            VStack {
                Image(systemName: "star.fill")
                    .font(.largeTitle)
                Text("Go Premium")
                    .font(.title)
            }
        }
        .subscriptionStoreButtonLabel(.multiline)
        .subscriptionStorePickerItemBackground(.ultraThinMaterial)
    }
}

Offer Codes & Promotions

Redeem Offer Code

func redeemOfferCode() async {
    // Opens App Store offer code redemption sheet
    try? await AppStore.presentOfferCodeRedeemSheet()
}

Promotional Offers

func purchaseWithOffer(_ product: Product, offerID: String) async throws -> Transaction? {
    guard let offer = product.subscription?.promotionalOffers.first(where: { $0.id == offerID }) else {
        return nil
    }

    // Generate signature on your server
    let signatureData = try await generateSignature(for: offer)

    let result = try await product.purchase(options: [
        .promotionalOffer(
            offerID: offer.id,
            keyID: signatureData.keyID,
            nonce: signatureData.nonce,
            signature: signatureData.signature,
            timestamp: signatureData.timestamp
        )
    ])

    // Handle result...
}

Receipt Validation

App Store Server API

// Server-side validation recommended
// Use App Store Server API for subscription status
func validateReceipt() async throws -> Bool {
    guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
          FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
        return false
    }

    let receiptData = try Data(contentsOf: appStoreReceiptURL)
    let receiptString = receiptData.base64EncodedString()

    // Send to your server for validation
    return try await validateOnServer(receipt: receiptString)
}

Error Handling

enum StoreError: LocalizedError {
    case verificationFailed
    case purchaseFailed
    case productNotFound

    var errorDescription: String? {
        switch self {
        case .verificationFailed: return "Transaction verification failed"
        case .purchaseFailed: return "Purchase could not be completed"
        case .productNotFound: return "Product not found"
        }
    }
}

Testing

StoreKit Testing in Xcode

#if DEBUG
func clearPurchaseHistory() async {
    // Only works in sandbox/testing
    for await result in Transaction.currentEntitlements {
        guard case .verified(let transaction) = result else { continue }
        await transaction.finish()
    }
}
#endif

Apple Documentation