wp:paragraph
Have you ever launched your iOS app only to watch your in-app purchases crash and burn with a mysterious SKErrorDomain Error 4? You’re not alone. This frustrating error blocks transactions, confuses users, and slashes your revenue potential—but it doesn’t have to.
/wp:paragraph
wp:paragraph
Unlike generic iOS errors, SKErrorDomain Error 4 strikes specifically at your monetization pipeline. In this manual, I’ll explain exactly what causes this “clientInvalid” error and walk you through proven solutions that work in 2026. You’ll get concrete code examples, step-by-step troubleshooting, and prevention strategies that fix the problem for good.
/wp:paragraph
wp:paragraph
Let’s transform this revenue-blocking headache into a minor speed bump.
/wp:paragraph
wp:image {“id”:48137,”linkDestination”:”custom”,”align”:”center”}

/wp:image
wp:heading
What is SKErrorDomain Error 4? Understanding the “clientInvalid” Error
/wp:heading
wp:paragraph
SKErrorDomain Error 4 (officially labeled as “clientInvalid”) occurs when iOS’s StoreKit framework determines that the client attempting to make a purchase isn’t authorized to complete the transaction. This error belongs to Apple’s StoreKit error domain, which handles all in-app purchase operations in iOS applications.
/wp:paragraph
wp:paragraph
When this error triggers, your app receives an error object that looks something like this:
/wp:paragraph
wp:paragraph
Error Domain=SKErrorDomain Code=4
/wp:paragraph
wp:paragraph
“Client is not authorized to make purchases”
/wp:paragraph
wp:paragraph
UserInfo={NSLocalizedDescription=Client is not authorized to make purchases}
/wp:paragraph
wp:paragraph
The key detail is error code 4, which indicates client-side authorization problems rather than server issues or product configuration errors. This distinction matters because it narrows down where to focus your troubleshooting.
/wp:paragraph
wp:paragraph
In practical terms, this means something about the user’s device setup, Apple ID configuration, or purchase restrictions preventing the transaction from proceeding. The issue could stem from parental controls, payment method problems, or specific account limitations requiring different handling strategies.
/wp:paragraph
wp:image {“id”:48133,”linkDestination”:”custom”,”align”:”center”}

/wp:image
wp:heading
What Triggers SKErrorDomain Error 4? Root Causes Explained
/wp:heading
wp:paragraph
Understanding what causes SKErrorDomain Error 4 helps you implement targeted solutions rather than generic fixes. Here are the most common triggers:
/wp:paragraph
wp:heading {“level”:3}
1. Parental Controls & Restrictions
/wp:heading
wp:paragraph
The most frequent cause involves device restrictions that explicitly block in-app purchases. These settings override your app’s purchase requests, immediately triggering the error.
/wp:paragraph
wp:paragraph
Problematic Scenario:
/wp:paragraph
wp:paragraph
// This standard purchase request will fail with Error 4 if restrictions are enabled
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
Prevention Solution:
/wp:paragraph
wp:paragraph
// Check for purchase capability before attempting transaction
/wp:paragraph
wp:paragraph
if SKPaymentQueue.canMakePayments() {
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
// Inform user about purchase restrictions
/wp:paragraph
wp:paragraph
showRestrictionsAlert()
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
2. Invalid Payment Methods
/wp:heading
wp:paragraph
When a user’s payment method is declined, expired, or doesn’t match their App Store region, SKErrorDomain Error 4 occurs. This is particularly common with temporary cards or accounts with regional mismatches.
/wp:paragraph
wp:paragraph
Common Problem Pattern:
/wp:paragraph
wp:paragraph
// Directly attempting purchase without validation
/wp:paragraph
wp:paragraph
func buyProduct(_ product: SKProduct) {
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
Improved Implementation:
/wp:paragraph
wp:paragraph
func buyProduct(_ product: SKProduct) {
/wp:paragraph
wp:paragraph
// First verify the user is logged in and can make purchases
/wp:paragraph
wp:paragraph
guard SKPaymentQueue.canMakePayments() else {
/wp:paragraph
wp:paragraph
showAuthorizationError()
/wp:paragraph
wp:paragraph
return
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Add proper error handling in your transaction observer
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// In your SKPaymentTransactionObserver
/wp:paragraph
wp:paragraph
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
/wp:paragraph
wp:paragraph
for transaction in transactions {
/wp:paragraph
wp:paragraph
switch transaction.transactionState {
/wp:paragraph
wp:paragraph
case .failed:
/wp:paragraph
wp:paragraph
if let error = transaction.error as? SKError, error.code == .clientInvalid {
/wp:paragraph
wp:paragraph
// Guide user to check payment method
/wp:paragraph
wp:paragraph
showPaymentMethodAlert()
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
queue.finishTransaction(transaction)
/wp:paragraph
wp:paragraph
// Handle other states…
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
3. App Store Sign-In Issues
/wp:heading
wp:paragraph
A surprisingly common cause is users not being signed adequately into their App Store accounts or using different accounts for the app and store.
/wp:paragraph
wp:paragraph
Detection Code:
/wp:paragraph
wp:paragraph
// Adding a specific check for App Store login status
/wp:paragraph
wp:paragraph
func verifyStoreAccountStatus(completion: @escaping (Bool) -> Void) {
/wp:paragraph
wp:paragraph
if #available(iOS 13.0, *) {
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().presentCodeRedemptionSheet()
/wp:paragraph
wp:paragraph
// If this doesn’t throw an error, user is signed in
/wp:paragraph
wp:paragraph
completion(true)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
// For older iOS versions, we can only check general payment capability
/wp:paragraph
wp:paragraph
completion(SKPaymentQueue.canMakePayments())
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
4. Sandbox Testing Environment Limitations
/wp:heading
wp:paragraph
SKErrorDomain Error 4 frequently appears during development due to sandbox testing constraints and misconfigured test accounts.
/wp:paragraph
wp:paragraph
Common Development Issue:
/wp:paragraph
wp:paragraph
// Testing without proper sandbox account setup
/wp:paragraph
wp:paragraph
func testPurchase() {
/wp:paragraph
wp:paragraph
// This will fail with Error 4 if sandbox account isn’t properly configured
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: testProduct)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
Solution:
/wp:paragraph
wp:paragraph
// Proper sandbox testing approach
/wp:paragraph
wp:paragraph
func testPurchase() {
/wp:paragraph
wp:paragraph
// First verify we’re in sandbox environment
/wp:paragraph
wp:paragraph
if let receiptURL = Bundle.main.appStoreReceiptURL,
/wp:paragraph
wp:paragraph
receiptURL.lastPathComponent == “sandboxReceipt” {
/wp:paragraph
wp:paragraph
print(“Running in sandbox environment – ensure test account is properly configured”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Then proceed with proper error handling
/wp:paragraph
wp:paragraph
if SKPaymentQueue.canMakePayments() {
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: testProduct)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
print(“Sandbox test account cannot make payments – check configuration”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading
Prevention vs. Recovery Strategies for SKErrorDomain Error 4
/wp:heading
wp:table
| Prevention Techniques | Recovery Strategies |
| Always check SKPaymentQueue.canMakePayments() before attempting purchases | Present clear error messaging guiding users to check restrictions |
| Implement receipt validation to verify the purchase environment | Provide direct App Store settings links to fix payment methods |
| Use StoreKit Testing in Xcode to simulate various error conditions | Cache failed purchase requests for retry after authorization is fixed |
| Monitor transaction observer errors proactively | Implement graceful fallbacks when purchases fail |
| Verify product identifiers before purchase attempts | Guide users through account verification steps |
| Test across multiple Apple ID types (restricted, full access) | Offer alternative payment restoration flows |
/wp:table
wp:image {“id”:48134,”linkDestination”:”custom”,”align”:”center”}

/wp:image
wp:heading
Diagnosing SKErrorDomain Error 4: Step-by-Step Troubleshooting
/wp:heading
wp:paragraph
When SKErrorDomain Error 4 appears, follow this systematic approach to identify the exact cause:
/wp:paragraph
wp:heading {“level”:3}
1. Enable Enhanced StoreKit Logging
/wp:heading
wp:paragraph
Start by implementing detailed StoreKit logging to capture the full context of the error:
/wp:paragraph
wp:paragraph
// Enable enhanced StoreKit logging
/wp:paragraph
wp:paragraph
func enableDetailedStoreKitLogging() {
/wp:paragraph
wp:paragraph
if #available(iOS 14.0, *) {
/wp:paragraph
wp:paragraph
// Use the newer unified logging system
/wp:paragraph
wp:paragraph
os_log(“StoreKit Transaction Beginning”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “storekit”), type: .debug)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
// Fallback for older iOS versions
/wp:paragraph
wp:paragraph
print(“[StoreKit] Transaction Beginning”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Log specific error details
/wp:paragraph
wp:paragraph
func logStoreKitError(_ error: Error) {
/wp:paragraph
wp:paragraph
if let skError = error as? SKError, skError.code == .clientInvalid {
/wp:paragraph
wp:paragraph
let errorCode = skError.code.rawValue
/wp:paragraph
wp:paragraph
let errorDescription = skError.localizedDescription
/wp:paragraph
wp:paragraph
print(“SKErrorDomain Error: Code \(errorCode) – \(errorDescription)”)
/wp:paragraph
wp:paragraph
// Log additional context
/wp:paragraph
wp:paragraph
print(“User Account Status: \(SKPaymentQueue.canMakePayments() ? “Can make payments” : “Cannot make payments”)”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
Real error log example:
/wp:paragraph
wp:paragraph
[StoreKit] Transaction Failed: Error Domain=SKErrorDomain Code=4 “Client is not authorized to make purchases” UserInfo={NSLocalizedDescription=Client is not authorized to make purchases}
/wp:paragraph
wp:paragraph
User Account Status: Cannot make payments
/wp:paragraph
wp:heading {“level”:3}
2. Create a Test Transaction
/wp:heading
wp:paragraph
Implement a diagnostic purchase method that captures specific error details:
/wp:paragraph
wp:paragraph
func runDiagnosticPurchase(for productID: String, completion: @escaping (SKErrorDomain?) -> Void) {
/wp:paragraph
wp:paragraph
// First fetch the product
/wp:paragraph
wp:paragraph
let request = SKProductsRequest(productIdentifiers: [productID])
/wp:paragraph
wp:paragraph
request.delegate = self
/wp:paragraph
wp:paragraph
request.start()
/wp:paragraph
wp:paragraph
// In the delegate method:
/wp:paragraph
wp:paragraph
// func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
/wp:paragraph
wp:paragraph
// if let product = response.products.first {
/wp:paragraph
wp:paragraph
// // Attempt purchase with error trapping
/wp:paragraph
wp:paragraph
// let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
// SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
// }
/wp:paragraph
wp:paragraph
// }
/wp:paragraph
wp:paragraph
// Then in your transaction observer:
/wp:paragraph
wp:paragraph
// if transaction.transactionState == .failed, let error = transaction.error as? SKError {
/wp:paragraph
wp:paragraph
// completion(error)
/wp:paragraph
wp:paragraph
// }
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
3. Check Device Restrictions Status
/wp:heading
wp:paragraph
Determine if restrictions are enabled with this utility method:
/wp:paragraph
wp:paragraph
func checkPurchaseRestrictions(completion: @escaping (Bool, String) -> Void) {
/wp:paragraph
wp:paragraph
if SKPaymentQueue.canMakePayments() {
/wp:paragraph
wp:paragraph
completion(false, “No purchase restrictions detected”)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
completion(true, “Purchase restrictions are active on this device”)
/wp:paragraph
wp:paragraph
// Additional diagnostics for iOS 14+
/wp:paragraph
wp:paragraph
if #available(iOS 14.0, *) {
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().presentCodeRedemptionSheet()
/wp:paragraph
wp:paragraph
// This will fail with a specific error if there are account issues
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
4. Verify Receipt Environment
/wp:heading
wp:paragraph
Determine if you’re in production or sandbox to narrow down potential causes:
/wp:paragraph
wp:paragraph
swift
/wp:paragraph
wp:paragraph
func checkReceiptEnvironment() -> String {
/wp:paragraph
wp:paragraph
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
/wp:paragraph
wp:paragraph
return “No receipt found”
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
if receiptURL.lastPathComponent == “sandboxReceipt” {
/wp:paragraph
wp:paragraph
return “Sandbox environment”
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
return “Production environment”
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:image {“id”:48135,”linkDestination”:”custom”,”align”:”center”}

/wp:image
wp:heading
Implementing Robust SKErrorDomain Error 4 Handling
/wp:heading
wp:paragraph
Let’s build a comprehensive solution that prevents, detects, and handles SKErrorDomain Error 4 effectively:
/wp:paragraph
wp:paragraph
import StoreKit
/wp:paragraph
wp:paragraph
import os.log
/wp:paragraph
wp:paragraph
class PurchaseManager: NSObject, SKPaymentTransactionObserver {
/wp:paragraph
wp:paragraph
// Singleton instance
/wp:paragraph
wp:paragraph
static let shared = PurchaseManager()
/wp:paragraph
wp:paragraph
// Completion handler typealias
/wp:paragraph
wp:paragraph
typealias PurchaseCompletion = (Bool, Error?) -> Void
/wp:paragraph
wp:paragraph
// Active purchase completions
/wp:paragraph
wp:paragraph
private var purchaseCompletions: [String: PurchaseCompletion] = [:]
/wp:paragraph
wp:paragraph
// MARK: – Initialization
/wp:paragraph
wp:paragraph
private override init() {
/wp:paragraph
wp:paragraph
super.init()
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(self)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
deinit {
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().remove(self)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – Public Methods
/wp:paragraph
wp:paragraph
/// Validates if purchases are possible before attempting them
/wp:paragraph
wp:paragraph
func canMakePurchases() -> Bool {
/wp:paragraph
wp:paragraph
return SKPaymentQueue.canMakePayments()
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
/// Comprehensive pre-purchase validation
/wp:paragraph
wp:paragraph
func validatePurchaseCapability() -> (canPurchase: Bool, reason: String?) {
/wp:paragraph
wp:paragraph
if !SKPaymentQueue.canMakePayments() {
/wp:paragraph
wp:paragraph
return (false, “Purchase restrictions are enabled on this device”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Check receipt environment for additional context
/wp:paragraph
wp:paragraph
let environment = checkReceiptEnvironment()
/wp:paragraph
wp:paragraph
if environment == “Sandbox environment” {
/wp:paragraph
wp:paragraph
// Log this for debugging but don’t prevent the purchase
/wp:paragraph
wp:paragraph
os_log(“Running in sandbox environment”, type: .debug)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
return (true, nil)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
/// Attempt to purchase a product with proper error handling
/wp:paragraph
wp:paragraph
func purchase(productID: String, completion: @escaping PurchaseCompletion) {
/wp:paragraph
wp:paragraph
// Validate purchase capability first
/wp:paragraph
wp:paragraph
let capability = validatePurchaseCapability()
/wp:paragraph
wp:paragraph
guard capability.canPurchase else {
/wp:paragraph
wp:paragraph
// Handle SKErrorDomain Error 4 preemptively
/wp:paragraph
wp:paragraph
let error = NSError(domain: SKErrorDomain,
/wp:paragraph
wp:paragraph
code: SKError.clientInvalid.rawValue,
/wp:paragraph
wp:paragraph
userInfo: [NSLocalizedDescriptionKey: capability.reason ?? “Client is not authorized to make purchases”])
/wp:paragraph
wp:paragraph
completion(false, error)
/wp:paragraph
wp:paragraph
return
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Fetch the product first
/wp:paragraph
wp:paragraph
let productRequest = SKProductsRequest(productIdentifiers: [productID])
/wp:paragraph
wp:paragraph
productRequest.delegate = self
/wp:paragraph
wp:paragraph
// Store completion handler for later
/wp:paragraph
wp:paragraph
purchaseCompletions[productID] = completion
/wp:paragraph
wp:paragraph
// Start the request
/wp:paragraph
wp:paragraph
productRequest.start()
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – Private Methods
/wp:paragraph
wp:paragraph
private func checkReceiptEnvironment() -> String {
/wp:paragraph
wp:paragraph
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
/wp:paragraph
wp:paragraph
return “No receipt found”
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
if receiptURL.lastPathComponent == “sandboxReceipt” {
/wp:paragraph
wp:paragraph
return “Sandbox environment”
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
return “Production environment”
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
private func handleClientInvalidError(transaction: SKPaymentTransaction, completion: PurchaseCompletion?) {
/wp:paragraph
wp:paragraph
let productID = transaction.payment.productIdentifier
/wp:paragraph
wp:paragraph
// Log detailed diagnostics
/wp:paragraph
wp:paragraph
os_log(“SKErrorDomain Error 4 occurred for product: %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “errors”), type: .error, productID)
/wp:paragraph
wp:paragraph
// Create a user-friendly error object
/wp:paragraph
wp:paragraph
let userInfo: [String: Any] = [
/wp:paragraph
wp:paragraph
NSLocalizedDescriptionKey: “Unable to complete purchase”,
/wp:paragraph
wp:paragraph
NSLocalizedRecoverySuggestionErrorKey: “Please check your device restrictions and payment method in Settings → [Your Name] → Payment & Shipping.”
/wp:paragraph
wp:paragraph
]
/wp:paragraph
wp:paragraph
let clientInvalidError = NSError(domain: SKErrorDomain, code: SKError.clientInvalid.rawValue, userInfo: userInfo)
/wp:paragraph
wp:paragraph
// Invoke completion handler
/wp:paragraph
wp:paragraph
completion?(false, clientInvalidError)
/wp:paragraph
wp:paragraph
// Finish the transaction
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().finishTransaction(transaction)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – SKPaymentTransactionObserver
/wp:paragraph
wp:paragraph
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
/wp:paragraph
wp:paragraph
for transaction in transactions {
/wp:paragraph
wp:paragraph
let productID = transaction.payment.productIdentifier
/wp:paragraph
wp:paragraph
let completion = purchaseCompletions[productID]
/wp:paragraph
wp:paragraph
switch transaction.transactionState {
/wp:paragraph
wp:paragraph
case .purchasing:
/wp:paragraph
wp:paragraph
os_log(“Transaction in progress for %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “transactions”), type: .debug, productID)
/wp:paragraph
wp:paragraph
case .purchased, .restored:
/wp:paragraph
wp:paragraph
os_log(“Transaction successful for %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “transactions”), type: .info, productID)
/wp:paragraph
wp:paragraph
completion?(true, nil)
/wp:paragraph
wp:paragraph
purchaseCompletions.removeValue(forKey: productID)
/wp:paragraph
wp:paragraph
queue.finishTransaction(transaction)
/wp:paragraph
wp:paragraph
case .failed:
/wp:paragraph
wp:paragraph
os_log(“Transaction failed for %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “transactions”), type: .error, productID)
/wp:paragraph
wp:paragraph
if let error = transaction.error as? SKError, error.code == .clientInvalid {
/wp:paragraph
wp:paragraph
// Handle SKErrorDomain Error 4 specifically
/wp:paragraph
wp:paragraph
handleClientInvalidError(transaction: transaction, completion: completion)
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
// Handle other errors
/wp:paragraph
wp:paragraph
completion?(false, transaction.error)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
purchaseCompletions.removeValue(forKey: productID)
/wp:paragraph
wp:paragraph
queue.finishTransaction(transaction)
/wp:paragraph
wp:paragraph
case .deferred:
/wp:paragraph
wp:paragraph
os_log(“Transaction deferred for %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “transactions”), type: .info, productID)
/wp:paragraph
wp:paragraph
@unknown default:
/wp:paragraph
wp:paragraph
os_log(“Unknown transaction state for %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “transactions”), type: .error, productID)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – SKProductsRequestDelegate Extension
/wp:paragraph
wp:paragraph
extension PurchaseManager: SKProductsRequestDelegate {
/wp:paragraph
wp:paragraph
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
/wp:paragraph
wp:paragraph
if response.products.isEmpty {
/wp:paragraph
wp:paragraph
for productID in response.invalidProductIdentifiers {
/wp:paragraph
wp:paragraph
if let completion = purchaseCompletions[productID] {
/wp:paragraph
wp:paragraph
let error = NSError(domain: SKErrorDomain, code: SKError.unknown.rawValue, userInfo: [NSLocalizedDescriptionKey: “Invalid product identifier: \(productID)”])
/wp:paragraph
wp:paragraph
completion(false, error)
/wp:paragraph
wp:paragraph
purchaseCompletions.removeValue(forKey: productID)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
return
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Process valid products
/wp:paragraph
wp:paragraph
for product in response.products {
/wp:paragraph
wp:paragraph
if let completion = purchaseCompletions[product.productIdentifier] {
/wp:paragraph
wp:paragraph
// Attempt purchase with the valid product
/wp:paragraph
wp:paragraph
let payment = SKPayment(product: product)
/wp:paragraph
wp:paragraph
SKPaymentQueue.default().add(payment)
/wp:paragraph
wp:paragraph
// Note: Don’t call completion here – wait for transaction observer
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
func request(_ request: SKRequest, didFailWithError error: Error) {
/wp:paragraph
wp:paragraph
os_log(“Product request failed: %{public}@”, log: OSLog(subsystem: “com.yourapp.purchases”, category: “products”), type: .error, error.localizedDescription)
/wp:paragraph
wp:paragraph
// Extract product ID from the request if possible
/wp:paragraph
wp:paragraph
if let productRequest = request as? SKProductsRequest,
/wp:paragraph
wp:paragraph
let productIDs = productRequest.value(forKey: “productIdentifiers”) as? Set<String>,
/wp:paragraph
wp:paragraph
let productID = productIDs.first {
/wp:paragraph
wp:paragraph
if let completion = purchaseCompletions[productID] {
/wp:paragraph
wp:paragraph
completion(false, error)
/wp:paragraph
wp:paragraph
purchaseCompletions.removeValue(forKey: productID)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:heading {“level”:3}
Integration in Your View Controller
/wp:heading
wp:paragraph
Here’s how to use the above manager in your UI:
/wp:paragraph
wp:paragraph
import UIKit
/wp:paragraph
wp:paragraph
import StoreKit
/wp:paragraph
wp:paragraph
class StoreViewController: UIViewController {
/wp:paragraph
wp:paragraph
// MARK: – Properties
/wp:paragraph
wp:paragraph
let productID = “com.yourapp.premium_subscription”
/wp:paragraph
wp:paragraph
// MARK: – UI Elements
/wp:paragraph
wp:paragraph
lazy var purchaseButton: UIButton = {
/wp:paragraph
wp:paragraph
let button = UIButton(type: .system)
/wp:paragraph
wp:paragraph
button.setTitle(“Purchase Premium”, for: .normal)
/wp:paragraph
wp:paragraph
button.addTarget(self, action: #selector(purchaseTapped), for: .touchUpInside)
/wp:paragraph
wp:paragraph
return button
/wp:paragraph
wp:paragraph
}()
/wp:paragraph
wp:paragraph
// MARK: – Lifecycle
/wp:paragraph
wp:paragraph
override func viewDidLoad() {
/wp:paragraph
wp:paragraph
super.viewDidLoad()
/wp:paragraph
wp:paragraph
setupUI()
/wp:paragraph
wp:paragraph
// Check purchase capability immediately and update UI
/wp:paragraph
wp:paragraph
updatePurchaseButtonState()
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – UI Setup
/wp:paragraph
wp:paragraph
private func setupUI() {
/wp:paragraph
wp:paragraph
// Add purchase button to view hierarchy
/wp:paragraph
wp:paragraph
view.addSubview(purchaseButton)
/wp:paragraph
wp:paragraph
// Set constraints…
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – Actions
/wp:paragraph
wp:paragraph
@objc private func purchaseTapped() {
/wp:paragraph
wp:paragraph
// Show activity indicator
/wp:paragraph
wp:paragraph
showLoadingIndicator()
/wp:paragraph
wp:paragraph
// Attempt purchase
/wp:paragraph
wp:paragraph
PurchaseManager.shared.purchase(productID: productID) { [weak self] success, error in
/wp:paragraph
wp:paragraph
// Hide activity indicator
/wp:paragraph
wp:paragraph
self?.hideLoadingIndicator()
/wp:paragraph
wp:paragraph
if success {
/wp:paragraph
wp:paragraph
// Handle successful purchase
/wp:paragraph
wp:paragraph
self?.showPurchaseSuccessUI()
/wp:paragraph
wp:paragraph
} else if let skError = error as? SKError, skError.code == .clientInvalid {
/wp:paragraph
wp:paragraph
// Handle SKErrorDomain Error 4 specifically
/wp:paragraph
wp:paragraph
self?.showClientInvalidErrorUI()
/wp:paragraph
wp:paragraph
} else {
/wp:paragraph
wp:paragraph
// Handle other errors
/wp:paragraph
wp:paragraph
self?.showGenericErrorUI(error: error)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// MARK: – Helper Methods
/wp:paragraph
wp:paragraph
private func updatePurchaseButtonState() {
/wp:paragraph
wp:paragraph
let capability = PurchaseManager.shared.validatePurchaseCapability()
/wp:paragraph
wp:paragraph
purchaseButton.isEnabled = capability.canPurchase
/wp:paragraph
wp:paragraph
if !capability.canPurchase {
/wp:paragraph
wp:paragraph
// Show a hint about the restriction
/wp:paragraph
wp:paragraph
showRestrictionHint(message: capability.reason ?? “Purchases are restricted on this device”)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
private func showClientInvalidErrorUI() {
/wp:paragraph
wp:paragraph
let alert = UIAlertController(
/wp:paragraph
wp:paragraph
title: “Purchase Not Allowed”,
/wp:paragraph
wp:paragraph
message: “Your device settings or Apple ID prevent making purchases. Please check:\n\n1. Restrictions in Settings → Screen Time → Content & Privacy\n2. Your payment method in Settings → [Your Name] → Payment & Shipping”,
/wp:paragraph
wp:paragraph
preferredStyle: .alert
/wp:paragraph
wp:paragraph
)
/wp:paragraph
wp:paragraph
// Add action to open Settings
/wp:paragraph
wp:paragraph
alert.addAction(UIAlertAction(title: “Open Settings”, style: .default) { _ in
/wp:paragraph
wp:paragraph
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
/wp:paragraph
wp:paragraph
UIApplication.shared.open(settingsURL)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
})
/wp:paragraph
wp:paragraph
alert.addAction(UIAlertAction(title: “Cancel”, style: .cancel))
/wp:paragraph
wp:paragraph
present(alert, animated: true)
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:paragraph
// Other UI helper methods…
/wp:paragraph
wp:paragraph
}
/wp:paragraph
wp:image {“id”:48136,”linkDestination”:”custom”,”align”:”center”}

/wp:image
wp:heading
Conclusion
/wp:heading
wp:paragraph
SKErrorDomain Error 4 signals a client authorization issue that prevents in-app purchases from completing. The most effective solution is pre-emptive validation using SKPaymentQueue.canMakePayments() before attempting transactions, coupled with clear user guidance when restrictions are detected.
/wp:paragraph
wp:paragraph
For developers, implement the comprehensive PurchaseManager class from this guide to handle this error gracefully. Always check purchase capability first, then provide specific guidance to help users resolve their authorization issue—parental controls, payment methods, or App Store account problems.
/wp:paragraph