Apple's Framework to support in-app purchases and interaction with the App Store

Overview

https://www.youtube.com/watch?v=a-tEj0QYExc&t=662s

https://www.youtube.com/watch?v=a-tEj0QYExc&t=662s

What is StoreKit?

Apple's Framework to support in-app purchases and interaction with the App Store.

What can we do with it?

  • In-App Purchase
  • Apple Music
  • Recommendations and reviews

How does In-App Purchasing work in iOS?

  1. Sign In into App Store Connect
  2. Register Your App
  3. Define In-App purchase
  4. Create Sandbox Accounts

What changed since WWDC 2020?

  1. Open Xcode
  2. Create Storekit Configuration File
  3. Start Coding and Testing Purchases

How does it work?

In App Purchase Tutorial, SwiftUI, All Products Covered

Create StoreKit Configuration items

Untitled

StoreKit Configuration → Only For TESETING!!!

Untitled

Code

SKProduct? { return fetchedProducts.first(where: { $0.productIdentifier == identifier }) } func purchaseProduct(_ product: SKProduct) { startObservingPaymentQueue() buy(product) { _ in } } func restorePurchases() { SKPaymentQueue.default().restoreCompletedTransactions() } } extension Store: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { var shouldFinishTransaction = false switch transaction.transactionState { case .purchased, .restored: completedPurchases.append(transaction.payment.productIdentifier) shouldFinishTransaction = true case .failed: shouldFinishTransaction = true case .deferred, .purchasing: break @unknown default: break } if shouldFinishTransaction { SKPaymentQueue.default().finishTransaction(transaction) DispatchQueue.main.async { [weak self] in self?.purchaseCompletionHandler?(transaction) self?.purchaseCompletionHandler = nil } } } // if !completedPurchases.isEmpty { // UserDefaults.standard.setValue(completedPurchases, forKey: "completedPurchase") // } } } extension Store: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { let loadedProducts = response.products let invalidProducts = response.invalidProductIdentifiers guard !loadedProducts.isEmpty else { print("Could not load the products!") if !invalidProducts.isEmpty { print("Invalid Products found: \(invalidProducts)") } productsRequest = nil return } // Cache the fetched products fetchedProducts = loadedProducts // Notify anyone waiting on the product load DispatchQueue.main.async { [weak self] in self?.fetchCompletionHandler?(loadedProducts) self?.fetchCompletionHandler = nil self?.productsRequest = nil } } } ">
//
//  Store.swift
//  SwiftUIInApp
//
//  Created by paige on 2021/11/15.
//

import StoreKit

typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)

class Store: NSObject, ObservableObject {
    
    @Published var allRecipes = [Recipe]()
    
    private let allProductIdentifiers = Set(["com.product.berryblue", "com.product.lemonberry"])
    
    private var completedPurchases = [String]() {
        didSet {
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                for index in self.allRecipes.indices {
                    self.allRecipes[index].isLocked = !self.completedPurchases.contains(self.allRecipes[index].id)
                }
            }
        }
    }
    private var productsRequest: SKProductsRequest?
    private var fetchedProducts = [SKProduct]()
    private var fetchCompletionHandler: FetchCompletionHandler? // fetch product
    private var purchaseCompletionHandler: PurchaseCompletionHandler?
    
    override init() {
        super.init()
        startObservingPaymentQueue()
        fetchProducts { products in
            self.allRecipes = products.map { Recipe(product: $0) }
        }
    }
    
    private func startObservingPaymentQueue() {
        SKPaymentQueue.default().add(self)
    }
    
    private func fetchProducts(_ completion: @escaping FetchCompletionHandler) {
        guard self.productsRequest == nil else { return }
        fetchCompletionHandler = completion
        productsRequest = SKProductsRequest(productIdentifiers: allProductIdentifiers)
        productsRequest?.delegate = self
        productsRequest?.start()
    }
    
    private func buy(_ product: SKProduct, completion: @escaping PurchaseCompletionHandler) {
        purchaseCompletionHandler = completion
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }
    
}

extension Store {
    
    func product(for identifier: String) -> SKProduct? {
        return fetchedProducts.first(where: { $0.productIdentifier == identifier })
    }
    
    func purchaseProduct(_ product: SKProduct) {
        startObservingPaymentQueue()
        buy(product) { _ in
            
        }
    }
    
    func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
}

extension Store: SKPaymentTransactionObserver {
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            var shouldFinishTransaction = false
            switch transaction.transactionState {
            case .purchased, .restored:
                completedPurchases.append(transaction.payment.productIdentifier)
                shouldFinishTransaction = true
            case .failed:
                shouldFinishTransaction = true
            case .deferred, .purchasing:
                break
            @unknown default:
                break
            }
            
            if shouldFinishTransaction {
                SKPaymentQueue.default().finishTransaction(transaction)
                DispatchQueue.main.async { [weak self] in
                    self?.purchaseCompletionHandler?(transaction)
                    self?.purchaseCompletionHandler = nil
                }
            }
            
        }
        
//        if !completedPurchases.isEmpty {
//            UserDefaults.standard.setValue(completedPurchases, forKey: "completedPurchase")
//        }
        
    }
    
}

extension Store: SKProductsRequestDelegate {
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        let loadedProducts = response.products
        let invalidProducts = response.invalidProductIdentifiers
        guard !loadedProducts.isEmpty else {
            print("Could not load the products!")
            if !invalidProducts.isEmpty {
                print("Invalid Products found: \(invalidProducts)")
            }
            productsRequest = nil
            return
        }
        
        // Cache the fetched products
        fetchedProducts = loadedProducts
        
        // Notify anyone waiting on the product load
        DispatchQueue.main.async { [weak self] in
            self?.fetchCompletionHandler?(loadedProducts)
            self?.fetchCompletionHandler = nil
            self?.productsRequest = nil
        }
        
    }
    
}
//
//  Reecipe.swift
//  SwiftUIInApp
//
//  Created by paige on 2021/11/15.
//

import Foundation
import StoreKit

struct Recipe: Hashable {
    let id: String
    let title: String
    let description: String
    var isLocked: Bool
    var price: String?
    let locale: Locale
    let imageName: String
    
    lazy var formatter: NumberFormatter = {
       let nf = NumberFormatter()
        nf.numberStyle = .currency
        nf.locale = locale
       return nf
    }()
    
    init(product: SKProduct, isLock: Bool = true) {
        self.id = product.productIdentifier
        self.title = product.localizedTitle
        self.description = product.localizedDescription
        self.isLocked = isLock
        self.locale = product.priceLocale
        self.imageName = product.productIdentifier
        if isLocked {
            self.price = formatter.string(from: product.price)
        }
    }
}
Void var body: some View { HStack { ZStack { Image(recipe.imageName) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .cornerRadius(9) .opacity(recipe.isLocked ? 0.8 : 1) .blur(radius: recipe.isLocked ? 3.0 : 0) .padding() Image(systemName: "lock.fill") .font(.largeTitle) .opacity(recipe.isLocked ? 1: 0) } VStack(alignment: .leading) { Text(recipe.title) .font(.title) Text(recipe.description) .font(.caption) } Spacer() if let price = recipe.price, recipe.isLocked { Button(action: action) { Text(price) .foregroundColor(.white) .padding(.horizontal) .padding(.vertical) .background(Color.black) .cornerRadius(25) } } } } } ">
//
//  ContentView.swift
//  SwiftUIInApp
//
//  Created by paige on 2021/11/15.
//

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject private var store: Store 
    

    var body: some View {
        NavigationView {
            List(store.allRecipes, id: \.self) { recipe in
                Group {
                    if !recipe.isLocked {
                        NavigationLink(destination: Text("Secret Recipe")) {
                            Row(recipe: recipe) {
                                    
                            }
                        }
                    } else {
                        Row(recipe: recipe) {
                            if let product = store.product(for: recipe.id) {
                                store.purchaseProduct(product)
                            }
                        }
                    }
                }
                .navigationBarItems(trailing: Button("Restore") {
                    store.restorePurchases()
                })
            }
            .navigationTitle("Recipe Store")
            
        }
    }
}

struct Row: View {
    let recipe: Recipe
    let action: () -> Void
    var body: some View {
        HStack {
            ZStack {
                Image(recipe.imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 80, height: 80)
                    .cornerRadius(9)
                    .opacity(recipe.isLocked ? 0.8 : 1)
                    .blur(radius: recipe.isLocked ? 3.0 : 0)
                    .padding()
                Image(systemName: "lock.fill")
                    .font(.largeTitle)
                    .opacity(recipe.isLocked ? 1: 0)
            }
            VStack(alignment: .leading) {
                Text(recipe.title)
                    .font(.title)
                Text(recipe.description)
                    .font(.caption)
            }
            Spacer()
            if let price = recipe.price, recipe.isLocked {
                Button(action: action) {
                    Text(price)
                        .foregroundColor(.white)
                        .padding(.horizontal)
                        .padding(.vertical)
                        .background(Color.black)
                        .cornerRadius(25)
                }
            }
        }
    }
    
}
You might also like...
AppVersion - Keep users on the up-to date version of your App.
AppVersion - Keep users on the up-to date version of your App.

📲 App Version Don't let you users to get stuck on outdated version of your app. Automatic update tracking using Semantic Versioning Buil-in UI alerts

🔥 🔥 🔥Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

🔥 🔥 🔥Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

🐵Fooling around with Apples private framework AvatarKit
🐵Fooling around with Apples private framework AvatarKit

Fooling around with Apples private framework AvatarKit, the framework used in Messages.app for recording Animoji videos. If you are looking to create your own Animoji, take a look at SBSCustomAnimoji.

JustPeek is an iOS Library that adds support for Force Touch-like Peek and Pop interactions on devices that do not natively support this kind of interaction.
JustPeek is an iOS Library that adds support for Force Touch-like Peek and Pop interactions on devices that do not natively support this kind of interaction.

JustPeek Warning: This library is not supported anymore by Just Eat. JustPeek is an iOS Library that adds support for Force Touch-like Peek and Pop in

Design and prototype customized UI, interaction, navigation, transition and animation for App Store ready Apps in Interface Builder with IBAnimatable.
Design and prototype customized UI, interaction, navigation, transition and animation for App Store ready Apps in Interface Builder with IBAnimatable.

Design and prototype customized UI, interaction, navigation, transition and animation for App Store ready Apps in Interface Builder with IBAnimatable.

In-app purchases and subscriptions made easy. Support for iOS, iPadOS, watchOS, and Mac.

In-app purchases and subscriptions made easy. Support for iOS, iPadOS, watchOS, and Mac.

SwiftyStoreKit is a lightweight In App Purchases framework for iOS, tvOS, watchOS, macOS, and Mac Catalyst ⛺
SwiftyStoreKit is a lightweight In App Purchases framework for iOS, tvOS, watchOS, macOS, and Mac Catalyst ⛺

SwiftyStoreKit is a lightweight In App Purchases framework for iOS, tvOS, watchOS, macOS, and Mac Catalyst. Features Super easy-to-use block-based API

Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺
Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺

SwiftyStoreKit is a lightweight In App Purchases framework for iOS, tvOS, watchOS, macOS, and Mac Catalyst. Features Super easy-to-use block-based API

C4 is an open-source creative coding framework that harnesses the power of native iOS programming with a simplified API that gets you working with media right away. Build artworks, design interfaces and explore new possibilities working with media and interaction.
MerchantKit - A modern In-App Purchases management framework for iOS developers

MerchantKit dramatically simplifies the work indie developers have to do in order to add premium monetizable components to their applications. Track purchased products, offer auto-renewing subscriptions, restore transactions, and much more.

A modern In-App Purchases management framework for iOS.

MerchantKit A modern In-App Purchases management framework for iOS developers. MerchantKit dramatically simplifies the work indie developers have to d

🍎 An App to check whether a non-App Store app is in App Store.
🍎 An App to check whether a non-App Store app is in App Store.

AppStorify 🍎 An App to check whether a non-App Store app is in App Store. Benfits Use App Store's upgrade mechanism instead of app's. App Store apps

Implementing and testing In-App Purchases with StoreKit2 in Xcode 13, Swift 5.5 and iOS 15.
Implementing and testing In-App Purchases with StoreKit2 in Xcode 13, Swift 5.5 and iOS 15.

StoreHelper Demo Implementing and testing In-App Purchases with StoreKit2 in Xcode 13, Swift 5.5, iOS 15. See also In-App Purchases with Xcode 12 and

Store-App - Store app made for IOS using Swift programming language
Store-App - Store app made for IOS using Swift programming language

Store-App Store app views products, cart, and using login from https://fakestore

Apphud SDK is a lightweight open-source Swift library to manage auto-renewable subscriptions and other in-app purchases in your iOS app.
Apphud SDK is a lightweight open-source Swift library to manage auto-renewable subscriptions and other in-app purchases in your iOS app.

Apphud SDK Apphud SDK is a lightweight open-source Swift library to manage auto-renewable subscriptions and other in-app purchases in your iOS app. No

Mercato is a lightweight In-App Purchases (StoreKit 2) library for iOS, tvOS, watchOS, macOS, and Mac Catalyst.
Mercato is a lightweight In-App Purchases (StoreKit 2) library for iOS, tvOS, watchOS, macOS, and Mac Catalyst.

Mercato Mercato is a lightweight In-App Purchases (StoreKit 2) library for iOS, tvOS, watchOS, macOS, and Mac Catalyst. Installation Swift Package Man

Client library for making in-app purchases on iOS and macOS Automattic apps

MobilePayKit Client library for making in-app purchases on iOS and macOS Automattic apps Introduction MobilePayKit is a client library for making in-a

daemon DICOMweb store proxying to pacs in c-store protocol

POSTCSTORE versionado versión autor comentario 2021-11-02 Jacques Fauquex versión inicial 2021-11-02 Jacques Fauquex aetLocal en la ruta del servicio.

A lightweight iOS library for In-App Purchases

#RMStore A lightweight iOS library for In-App Purchases. RMStore adds blocks and notifications to StoreKit, plus receipt verification, content downloa

Owner
paigeshin
paigeshin
MerchantKit - A modern In-App Purchases management framework for iOS developers

MerchantKit dramatically simplifies the work indie developers have to do in order to add premium monetizable components to their applications. Track purchased products, offer auto-renewing subscriptions, restore transactions, and much more.

Benjamin Mayo 1.1k Dec 17, 2022
🍎 An App to check whether a non-App Store app is in App Store.

AppStorify ?? An App to check whether a non-App Store app is in App Store. Benfits Use App Store's upgrade mechanism instead of app's. App Store apps

seedgou 58 Dec 7, 2022
Apphud SDK is a lightweight open-source Swift library to manage auto-renewable subscriptions and other in-app purchases in your iOS app.

Apphud SDK Apphud SDK is a lightweight open-source Swift library to manage auto-renewable subscriptions and other in-app purchases in your iOS app. No

Apphud 143 Dec 16, 2022
Mercato is a lightweight In-App Purchases (StoreKit 2) library for iOS, tvOS, watchOS, macOS, and Mac Catalyst.

Mercato Mercato is a lightweight In-App Purchases (StoreKit 2) library for iOS, tvOS, watchOS, macOS, and Mac Catalyst. Installation Swift Package Man

Pavel T 49 Jan 4, 2023
InAppPurchase - A Simple and Lightweight framework for In App Purchase

InAppPurchase A Simple, Lightweight and Safe framework for In App Purchase Feature Simple and Light ?? Support Promoting In-App Purchases ?? No need t

Jin Sasaki 269 Dec 15, 2022
An easy way to access reviews for your app instead of writing repetitive and redundant codes for every app.

AppStoreReviewManager An easy way to access reviews for your app instead of writing repetitive and redundant codes for every app. Requirements iOS 9.0

Jinya 4 Dec 23, 2022
Harpy - Notify users when a new version of your app is available and prompt them to upgrade.

After 6 years of Harpy and 4 years of Siren, I have decided to deprecate Harpy in favor of Siren. Why? Siren is written in Swift and has a feature set

Arthur Ariel Sabintsev 2.6k Dec 29, 2022
Siren - Notify users when a new version of your app is available and prompt them to upgrade.

Siren ?? Notify users when a new version of your app is available and prompt them to upgrade. Table of Contents Meta About Features Screenshots Ports

Arthur Ariel Sabintsev 4.1k Dec 27, 2022
An App where you can find product and see its detail.

MeliProductos Project ???? > An App where you can find product and see its detail. ???? > Una App donde puedes encontrar tus productos y ver su detall

Joaquin Segovia 3 May 6, 2022
Appirater - A utility that reminds your iPhone app's users to review the app.

Introduction Appirater is a class that you can drop into any iPhone app (iOS 4.0 or later) that will help remind your users to review your app on the

Arash Payan 4.7k Jan 7, 2023