SwiftyStoreKit

repository·master·Indexed 27 days ago

https://github.com/bizz84/swiftystorekit

A lightweight In-App Purchases framework for Apple platforms (iOS, tvOS, watchOS, macOS, and Mac Catalyst). It provides a block-based API for managing consumables, non-consumables, and subscriptions, including features for retrieving product information, handling pending transactions, restoring purchases, downloading Apple-hosted content, and verifying receipts and subscription statuses.

Tokens
4K
Snippets
11
Records
14
Agent score
42%

What's inside SwiftyStoreKit

  1. Install SwiftyStoreKit via Swift Package Manager

    master

    Swift Package Manager (SPM) is the recommended installation method. If you are using Xcode 11 or later, follow these steps:

    1. Click File in the menu bar.
    2. Select Swift Packages.
    3. Select Add Package Dependency....
    4. Enter the following git URL:
    https://github.com/bizz84/SwiftyStoreKit.git
  2. Download content hosted with Apple

    master

    To handle content hosted by Apple, use the downloads property on a transaction.

    1. Start the download using SwiftyStoreKit.start(downloads) within a purchase or restore completion block.
    2. Monitor progress by setting the SwiftyStoreKit.updatedDownloadsHandler in your AppDelegate.
    3. Once contentURL is available (indicating .finished state), process the files and call SwiftyStoreKit.finishTransaction(downloads[0].transaction).

    Control methods: start(), pause(), resume(), cancel().

    // 1. Start downloads
    SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", quantity: 1, atomically: false) { result in
        switch result {
        case .success(let product):
            let downloads = product.transaction.downloads
            if !downloads.isEmpty {
                SwiftyStoreKit.start(downloads)
            }
        case .error(let error):
            print("\(error)")
        }
    }
    
    // 2. Monitor in AppDelegate
    SwiftyStoreKit.updatedDownloadsHandler = { downloads in
        let contentURLs = downloads.flatMap { $0.contentURL }
        if contentURLs.count == downloads.count {
            // process all downloaded files, then finish the transaction
            SwiftyStoreKit.finishTransaction(downloads[0].transaction)
        }
    }
  3. Handle pending transactions on app startup

    master

    Apple recommends registering a transaction observer as soon as the app starts. SwiftyStoreKit provides completeTransactions(atomically:completion:) for this purpose.

    Important:

    • Call completeTransactions() only once in your application lifecycle, typically within application(_:didFinishLaunchingWithOptions:).
    • If there are pending transactions, the completion block is called with the list of purchases. If no transactions are pending, the completion block will not be called.
    • For each purchase, you should check the transactionState. If it is .purchased or .restored, deliver the content and then call SwiftyStoreKit.finishTransaction(_:) if purchase.needsFinishTransaction is true.
    import SwiftyStoreKit
    
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    	// see notes below for the meaning of Atomic / Non-Atomic
    	SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
    	    for purchase in purchases {
    	        switch purchase.transaction.transactionState {
    	        case .purchased, .restored:
    	            if purchase.needsFinishTransaction {
    	                // Deliver content from server, then:
    	                SwiftyStoreKit.finishTransaction(purchase.transaction)
    	            }
    	            // Unlock content
    	        case .failed, .purchasing, .deferred:
    	            break // do nothing
    	        }
    	    }
    	}
        return true
    }
  4. Restore previous purchases

    master

    Use SwiftyStoreKit.restorePurchases to restore non-consumable and auto-renewable subscription purchases.

    • Atomic: Use atomically: true for immediate content delivery.
    • Non-Atomic: Use atomically: false for server-side delivery. If using non-atomic, iterate through results.restoredPurchases and call SwiftyStoreKit.finishTransaction(purchase.transaction) if purchase.needsFinishTransaction is true.

    Results include restoredPurchases and restoreFailedPurchases.

    SwiftyStoreKit.restorePurchases(atomically: false) { results in
        if results.restoreFailedPurchases.count > 0 {
            print("Restore Failed: \(results.restoreFailedPurchases)")
        }
        else if results.restoredPurchases.count > 0 {
            for purchase in results.restoredPurchases {
                // fetch content from your server, then:
                if purchase.needsFinishTransaction {
                    SwiftyStoreKit.finishTransaction(purchase.transaction)
                }
            }
            print("Restore Success: \(results.restoredPurchases)")
        }
        else {
            print("Nothing to Restore")
        }
    }
  5. Retrieve product information

    master

    Use SwiftyStoreKit.retrieveProductsInfo to fetch metadata for a list of product identifiers. The result contains retrievedProducts (successful lookups) and invalidProductIDs (IDs that could not be found in the storefront).

    SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
        if let product = result.retrievedProducts.first {
            let priceString = product.localizedPrice!
            print("Product: \(product.localizedDescription), price: \(priceString)")
        }
        else if let invalidProductId = result.invalidProductIDs.first {
            print("Invalid product identifier: \(invalidProductId)")
        }
        else {
            print("Error: \(result.error)")
        }
    }
  6. Verify receipts

    master

    SwiftyStoreKit provides several ways to handle receipt verification:

    • Local Data: Access SwiftyStoreKit.localReceiptData for the current encrypted receipt.
    • Fetch Updated Receipt: Use SwiftyStoreKit.fetchReceipt(forceRefresh: true) to ensure the receipt is up-to-date before validation.
    • Full Verification: Use SwiftyStoreKit.verifyReceipt(using:forceRefresh:) with an AppleReceiptValidator to perform a complete validation against Apple's servers in one step.
    // Full verification example
    let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
    SwiftyStoreKit.verifyReceipt(using: appleValidator, forceRefresh: false) { result in
        switch result {
        case .success(let receipt):
            print("Verify receipt success: \(receipt)")
        case .error(let error):
            print("Verify receipt failed: \(error)")
        }
    }
  7. Purchase a product

    master

    Use SwiftyStoreKit.purchaseProduct to initiate a purchase.

    • Atomic: Set atomically: true when content is delivered immediately.
    • Non-Atomic: Set atomically: false when content is delivered by a server. In this mode, you must check product.needsFinishTransaction and call SwiftyStoreKit.finishTransaction(product.transaction) after your server-side logic is complete.

    Common error codes include .paymentCancelled, .paymentInvalid, .storeProductNotAvailable, and various .cloudService errors.

    // Non-Atomic Example (Server-side delivery)
    SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", quantity: 1, atomically: false) { result in
        switch result {
        case .success(let product):
            // fetch content from your server, then:
            if product.needsFinishTransaction {
                SwiftyStoreKit.finishTransaction(product.transaction)
            }
            print("Purchase Success: \(product.productId)")
        case .error(let error):
            // handle error
            break
        }
    }
  8. Verify a subscription status

    master

    Use verifySubscription(ofType:productId:inReceipt:) to check if a subscription is active, expired, or never purchased. The ofType parameter accepts .autoRenewable or .nonRenewing. When a subscription is found, it returns a ReceiptItem array ordered by expiryDate, with the newest item first.

    let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
    SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
        switch result {
        case .success(let receipt):
            let productId = "com.musevisions.SwiftyStoreKit.Subscription"
            // Verify the purchase of a Subscription
            let purchaseResult = SwiftyStoreKit.verifySubscription(
                ofType: .autoRenewable, // or .nonRenewing (see below)
                productId: productId,
                inReceipt: receipt)
                
            switch purchaseResult {
            case .purchased(let expiryDate, let items):
                print("\(productId) is valid until \(expiryDate)\n\(items)\n")
            case .expired(let expiryDate, let items):
                print("\(productId) is expired since \(expiryDate)\n\(items)\n")
            case .notPurchased:
                print("The user has never purchased \(productId)")
            }
    
        case .error(let error):
            print("Receipt verification failed: \(error)")
        }
    }
  9. Verify a single purchase (Consumable or Non-Consumable)

    master

    To verify a specific product purchase, first retrieve the receipt using verifyReceipt with an AppleReceiptValidator. Once the receipt is obtained, use verifyPurchase(productId:inReceipt:) to check the status of a specific product identifier.

    let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
    SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
        switch result {
        case .success(let receipt):
            let productId = "com.musevisions.SwiftyStoreKit.Purchase1"
            // Verify the purchase of Consumable or NonConsumable
            let purchaseResult = SwiftyStoreKit.verifyPurchase(
                productId: productId,
                inReceipt: receipt)
                
            switch purchaseResult {
            case .purchased(let receiptItem):
                print("\(productId) is purchased: \(receiptItem)")
            case .notPurchased:
                print("The user has never purchased \(productId)")
            }
        case .error(let error):
            print("Receipt verification failed: \(error)")
        }
    }
  10. Verify all subscriptions in a Subscription Group

    master
    A subscription group is a set of in-app purchases where users can only buy one subscription at a time. To verify all subscriptions belonging to the same group, use the verifySubscriptions method.