ios-in-app-purchases/SKILL.md
Implement in-app purchases and subscriptions using StoreKit 2. Use when adding purchases, implementing subscriptions, handling receipt validation, managing entitlements, or debugging StoreKit issues. Triggers on in-app purchase, IAP, StoreKit, subscription, purchase, receipt, entitlement, paywall, monetization.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-in-app-purchasesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an in-app purchases specialist using StoreKit 2 (iOS 15+). When this skill activates, help implement robust purchase handling.
| Type | Use Case | |------|----------| | Consumable | Coins, gems, one-time boosts | | Non-Consumable | Unlock features, remove ads, themes | | Auto-Renewable Subscription | Premium access, content subscriptions | | Non-Renewing Subscription | Season pass, time-limited access |
import StoreKit
@Observable
class StoreManager {
private(set) var products: [Product] = []
private(set) var purchasedProductIDs: Set<String> = []
private(set) var subscriptionGroupStatus: Product.SubscriptionInfo.Status?
private var updateListenerTask: Task<Void, Error>?
// Product identifiers
static let productIDs: Set<String> = [
"com.app.premium.monthly",
"com.app.premium.yearly",
"com.app.coins.100",
"com.app.removeads"
]
init() {
updateListenerTask = listenForTransactions()
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
deinit {
updateListenerTask?.cancel()
}
// MARK: - Load Products
func loadProducts() async {
do {
products = try await Product.products(for: Self.productIDs)
.sorted { $0.price < $1.price }
} catch {
print("Failed to load products: \(error)")
}
}
// MARK: - Purchase
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 .pending:
// Ask-to-Buy or other pending states
return nil
case .userCancelled:
return nil
@unknown default:
return nil
}
}
// MARK: - Restore Purchases
func restorePurchases() async throws {
try await AppStore.sync()
await updatePurchasedProducts()
}
// MARK: - Transaction Listener
private func listenForTransactions() -> Task<Void, Error> {
Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
await self.updatePurchasedProducts()
await transaction.finish()
} catch {
print("Transaction failed verification: \(error)")
}
}
}
}
// MARK: - Verification
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw error
case .verified(let item):
return item
}
}
// MARK: - Update Purchased Products
func updatePurchasedProducts() async {
var purchased: Set<String> = []
for await result in Transaction.currentEntitlements {
do {
let transaction = try checkVerified(result)
switch transaction.productType {
case .autoRenewable:
if let expirationDate = transaction.expirationDate,
expirationDate > Date() {
purchased.insert(transaction.productID)
}
case .nonConsumable:
if transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
default:
break
}
} catch {
print("Failed to verify transaction: \(error)")
}
}
await MainActor.run {
self.purchasedProductIDs = purchased
}
}
// MARK: - Subscription Status
func checkSubscriptionStatus() async {
guard let product = products.first(where: { $0.type == .autoRenewable }) else {
return
}
guard let statuses = try? await product.subscription?.status else {
return
}
subscriptionGroupStatus = statuses.first { status in
switch status.state {
case .subscribed, .inGracePeriod, .inBillingRetryPeriod:
return true
default:
return false
}
}
}
// MARK: - Helpers
var isPremium: Bool {
purchasedProductIDs.contains("com.app.premium.monthly") ||
purchasedProductIDs.contains("com.app.premium.yearly")
}
func product(for id: String) -> Product? {
products.first { $0.id == id }
}
}
struct PaywallView: View {
@Environment(StoreManager.self) private var store
@Environment(\.dismiss) private var dismiss
@State private var isPurchasing = false
@State private var error: Error?
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
// Header
VStack(spacing: 8) {
Image(systemName: "crown.fill")
.font(.system(size: 60))
.foregroundStyle(.yellow)
Text("Go Premium")
.font(.largeTitle.bold())
Text("Unlock all features")
.foregroundStyle(.secondary)
}
.padding(.top)
// Features
FeaturesList()
// Products
ForEach(subscriptionProducts) { product in
ProductCard(product: product) {
await purchase(product)
}
}
// Restore
Button("Restore Purchases") {
Task { await restore() }
}
.font(.footnote)
// Terms
Text("Subscriptions auto-renew unless cancelled 24 hours before the end of the current period.")
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding()
}
.padding()
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Close") { dismiss() }
}
}
.overlay {
if isPurchasing {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
}
}
.alert("Error", isPresented: .constant(error != nil)) {
Button("OK") { error = nil }
} message: {
Text(error?.localizedDescription ?? "")
}
}
}
private var subscriptionProducts: [Product] {
store.products.filter { $0.type == .autoRenewable }
}
private func purchase(_ product: Product) async {
isPurchasing = true
defer { isPurchasing = false }
do {
if let _ = try await store.purchase(product) {
dismiss()
}
} catch {
self.error = error
}
}
private func restore() async {
isPurchasing = true
defer { isPurchasing = false }
do {
try await store.restorePurchases()
if store.isPremium {
dismiss()
}
} catch {
self.error = error
}
}
}
struct ProductCard: View {
let product: Product
let onPurchase: () async -> Void
var body: some View {
VStack(spacing: 12) {
HStack {
VStack(alignment: .leading) {
Text(product.displayName)
.font(.headline)
Text(product.description)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(product.displayPrice)
.font(.title3.bold())
if let subscription = product.subscription {
Text(subscription.subscriptionPeriod.displayString)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
Button {
Task { await onPurchase() }
} label: {
Text("Subscribe")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(.quaternary, in: RoundedRectangle(cornerRadius: 12))
}
}
extension Product.SubscriptionPeriod {
var displayString: String {
switch unit {
case .day: return value == 1 ? "per day" : "per \(value) days"
case .week: return value == 1 ? "per week" : "per \(value) weeks"
case .month: return value == 1 ? "per month" : "per \(value) months"
case .year: return value == 1 ? "per year" : "per \(value) years"
@unknown default: return ""
}
}
}
// Simple entitlement check
extension StoreManager {
func hasEntitlement(for productID: String) -> Bool {
purchasedProductIDs.contains(productID)
}
var canAccessPremiumContent: Bool {
isPremium
}
}
// View modifier for premium content
struct PremiumOnlyModifier: ViewModifier {
@Environment(StoreManager.self) private var store
@State private var showPaywall = false
func body(content: Content) -> some View {
Group {
if store.isPremium {
content
} else {
Button("Unlock Premium") {
showPaywall = true
}
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
}
}
}
extension View {
func premiumOnly() -> some View {
modifier(PremiumOnlyModifier())
}
}
extension StoreManager {
func purchaseConsumable(_ product: Product) async throws -> Int {
guard product.type == .consumable else {
throw StoreError.wrongProductType
}
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
// Grant the consumable (e.g., add coins)
let quantity = transaction.purchasedQuantity
await addCoins(quantity * 100)
// Always finish consumables
await transaction.finish()
return quantity
case .pending, .userCancelled:
return 0
@unknown default:
return 0
}
}
@MainActor
private func addCoins(_ amount: Int) {
UserDefaults.standard.set(
(UserDefaults.standard.integer(forKey: "coins") + amount),
forKey: "coins"
)
}
}
// Open subscription management in App Store
func openSubscriptionManagement() async {
guard let windowScene = await UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return
}
do {
try await AppStore.showManageSubscriptions(in: windowScene)
} catch {
// Fallback to Settings URL
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
await UIApplication.shared.open(url)
}
}
}
extension StoreManager {
func subscriptionRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? {
guard let product = product(for: productID),
let subscription = product.subscription else {
return nil
}
guard let statuses = try? await subscription.status else {
return nil
}
for status in statuses {
if case .verified(let renewalInfo) = status.renewalInfo {
return renewalInfo
}
}
return nil
}
func willRenew(productID: String) async -> Bool {
guard let renewalInfo = await subscriptionRenewalInfo(for: productID) else {
return false
}
return renewalInfo.willAutoRenew
}
}
#if DEBUG
extension StoreManager {
func clearTestPurchases() async {
for await result in Transaction.all {
if case .verified(let transaction) = result {
await transaction.finish()
}
}
}
}
#endif
// Check environment
func isTestEnvironment() -> Bool {
#if DEBUG
return true
#else
return Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt"
#endif
}
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes