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?
- Sign In into App Store Connect
- Register Your App
- Define In-App purchase
- Create Sandbox Accounts
What changed since WWDC 2020?
- Open Xcode
- Create Storekit Configuration File
- Start Coding and Testing Purchases
How does it work?
In App Purchase Tutorial, SwiftUI, All Products Covered
Create StoreKit Configuration items
StoreKit Configuration → Only For TESETING!!!
Code
//
// 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)
}
}
}
//
// 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)
}
}
}
}
}