In app purchases

Hey guys,

This is a general question, but I have been working on an app for a while and I would like to to introduce IAP to the app. I have no clue where to start and chat GPT is no help. I also tried to vibe coding the IAP function and that didn't work out well. Any material or advice would help.

These links will explain everything:

Explainer: https://developer.apple.com/in-app-purchase/

IAPs with StoreKit: https://developer.apple.com/documentation/storekit/in-app-purchase

Create a Sandbox Apple Account: https://developer.apple.com/help/app-store-connect/test-in-app-purchases/create-a-sandbox-apple-account/

Product information for an IAP: https://developer.apple.com/help/app-store-connect/reference/in-app-purchases-and-subscriptions/in-app-purchase-information

Overview for configuring IAPs: https://developer.apple.com/help/app-store-connect/configure-in-app-purchase-settings/overview-for-configuring-in-app-purchases

View and edit In-App Purchase information: https://developer.apple.com/help/app-store-connect/manage-in-app-purchases/view-and-edit-in-app-purchase-information

Testing refund requests: https://developer.apple.com/documentation/storekit/testing-refund-requests

Note: You must test this on a physical device. In other words, create your sandbox Apple Account and sign in on a physical device. Deploy your app to that device to test the IAPs. You cannot test them in the Simulator.

This code might help you to start:

//  StoreProductController.swift

import Combine
import Foundation
import StoreKit

@MainActor
public final class StoreProductController: ObservableObject {
	@Published public internal(set) var product: Product?
	@Published public private(set) var isEntitled: Bool = false
	@Published public private(set) var purchaseError: Error?

	private let productID: String

	internal nonisolated init(identifiedBy productID: String) {
		self.productID = productID
		Task(priority: .background) {
			await self.updateEntitlement()
		}
	}

	public func purchase() async {
		guard let product = product else {
			return
		}

		do {
			let result = try await product.purchase()
			switch result {
				case .success(let verificationResult):
					switch(verificationResult) {
						case .verified(let transaction):
							// Give the user access to purchased content
							self.isEntitled = true
							await transaction.finish()

						case .unverified(let transaction, let verificationError):
							// Handle unverified transactions based on your business model
							print("purchase(): Unverified transaction: \(transaction.debugDescription), error: \(verificationError.localizedDescription)")
					}

				case .pending:
					// The purchase requires action from the customer. If the transaction completes, it's available through Transaction.updates
					break

				case .userCancelled:
					// The user cancelled the purchase
					break

				@unknown default:
					// Unknown result
					break
			}

		} catch {
			purchaseError = error
			print("purchase(): Error: \(error.localizedDescription)")
		}
	}

	internal func set(isEntitled: Bool) {
		self.isEntitled = isEntitled
	}

	private func updateEntitlement() async {
		switch await StoreKit.Transaction.currentEntitlement(for: productID) {
			case .verified:
				isEntitled = true
			case .unverified(_, let error):
				logger.info("Unverified entitlement for \(self.productID): \(error)")
				fallthrough
			case .none:
				isEntitled = false
		}
	}
}
// These can go anywhere
enum ProductNames: String, Codable {
	case free = "feature.free"
	case pro	= "feature.pro"	// This must match the IAP in App Store Connect
}

extension ProductNames {
	func humanReadablePurchaseLevel() -> String {
		switch self {
			case .free:
				return "Free"
			case .pro:
				return "Pro"
		}
	}
}
//  StoreActor.swift

import Foundation
import StoreKit

@globalActor public actor StoreActor {
	public static let featurePro = ProductNames.pro.rawValue
	static let allProductIDs: Set<String> = [featurePro]
	public static let shared = StoreActor()

	private var loadedProducts: [String: Product] = [:]
	private var lastLoadError: Error?
	private var productLoadingTask: Task<Void, Never>?

	private var transactionUpdatesTask: Task<Void, Never>?
	private var storefrontUpdatesTask: Task<Void, Never>?

	public nonisolated let productController: StoreProductController

	init() {
		self.productController = StoreProductController(identifiedBy: Self.featurePro)
		Task(priority: .background) {
			await self.setupListenerTasksIfNecessary()
			await self.loadProducts()
		}
	}

	public func product(identifiedBy productID: String) async -> Product? {
		await waitUntilProductsLoaded()
		return loadedProducts[productID]
	}

	private func setupListenerTasksIfNecessary() {
		if(transactionUpdatesTask == nil) {
			transactionUpdatesTask = Task(priority: .background) {
				for await update in StoreKit.Transaction.updates {
					await self.handle(transaction: update)
				}
			}
		}

		if(storefrontUpdatesTask == nil) {
			storefrontUpdatesTask = Task(priority: .background) {
				for await update in Storefront.updates {
					self.handle(storefrontUpdate: update)
				}
			}
		}
	}

	private func waitUntilProductsLoaded() async {
		if let task = productLoadingTask {
			await task.value

		} else if(loadedProducts.isEmpty) {
			// You load all the products at once, so you can skip this if the dictionary is empty
			let newTask = Task {
				await loadProducts()
			}
			productLoadingTask = newTask
			await newTask.value
		}
	}

	private func loadProducts() async {
		do {
			let products = try await Product.products(for: Self.allProductIDs)
			try Task.checkCancellation()
			loadedProducts = products.reduce(into: [:]) { $0[$1.id] = $1 }

			let premiumProduct = loadedProducts[Self.featurePro]
			Task(priority: .utility) { @MainActor in
				self.productController.product = premiumProduct
			}
		} catch {
			lastLoadError = error
		}
		productLoadingTask = nil
	}


	private func handle(transaction: VerificationResult<StoreKit.Transaction>) async {
		guard case .verified(let transaction) = transaction else {
			print("Received unverified transaction: \(transaction)")
			return
		}

		if(transaction.productType == .nonConsumable && transaction.productID == Self.featurePro) {
			await productController.set(isEntitled: !transaction.isRevoked)
			if(transaction.isRevoked) {
				// Refund has been issued; disable pro features
			}
		}

		await transaction.finish()
	}

	private func handle(storefrontUpdate newStorefront: Storefront) {
		// Cancel existing loading task if necessary
		if let task = productLoadingTask {
			task.cancel()
		}

		// Load products again
		productLoadingTask = Task(priority: .utility) {
			await self.loadProducts()
		}
	}
}

public extension StoreKit.Transaction {
    var isRevoked: Bool {
        // The revocation date is never in the future
        revocationDate != nil
    }
}
//  StoreView.swift

import StoreKit
import SwiftUI

struct StoreView: View {
	@ObservedObject private var storeController = StoreActor.shared.productController

	var hasPro: Bool {
		storeController.isEntitled
	}

	var body: some View {
		if let product = storeController.product, !storeController.isEntitled {
			// Pro features available to the user TO PURCHASE
			if(product.displayName == ProductNames.pro.humanReadablePurchaseLevel()) {
				// Show the product in a nice way, using the properties of the `product` variable
				ProductRow(product: product)
			}
		}

		if(hasPro) {
			// The user has already purchased the pro version, so show the pro features
		}
	}
}

struct ProductRow: View {
	@ObservedObject private var storeController = StoreActor.shared.productController
	var product: Product

	var body: some View {
		Text("Upgrade to \(product.displayName)")
		Text(product.description)

		// Purchase button
		Button {
			Task(priority: .userInitiated) {
				await storeController.purchase()
			}
		} label: {
			Text("\(product.displayPrice)")
		}
	}
}

As you've discovered, ChatGPT didn't help you, and "vibe coding" was a bust.

The real way to do these things is to ask a human. We're here to help. Always ask a human first.

Check out StoreKit 2 tutorials by searching online.

In app purchases
 
 
Q