-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from Isvvc/groups-iap
Groups in-app-purchase
- Loading branch information
Showing
10 changed files
with
362 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// | ||
// StoreController.swift | ||
// Tickmate | ||
// | ||
// Created by Isaac Lyons on 6/1/21. | ||
// | ||
|
||
import SwiftUI | ||
import StoreKit | ||
|
||
class StoreController: NSObject, ObservableObject { | ||
|
||
@Published private(set) var products = [SKProduct]() | ||
|
||
@Published private(set) var purchased = Set<String>() | ||
@Published private(set) var purchasing = Set<String>() | ||
@Published var restored: AlertItem? | ||
|
||
private var isRestoringPurchases = false | ||
|
||
var isAuthorizedForPayments: Bool { | ||
SKPaymentQueue.canMakePayments() | ||
} | ||
|
||
override init() { | ||
super.init() | ||
SKPaymentQueue.default().add(self) | ||
} | ||
|
||
var priceFormatter: NumberFormatter = { | ||
let formatter = NumberFormatter() | ||
formatter.formatterBehavior = .behavior10_4 | ||
formatter.numberStyle = .currency | ||
return formatter | ||
}() | ||
|
||
func fetchProducts() { | ||
let request = SKProductsRequest(productIdentifiers: Set(Products.allValues)) | ||
request.delegate = self | ||
request.start() | ||
} | ||
|
||
func purchase(_ product: SKProduct) { | ||
guard isAuthorizedForPayments else { return } | ||
|
||
let payment = SKPayment(product: product) | ||
SKPaymentQueue.default().add(payment) | ||
} | ||
|
||
func restorePurchases() { | ||
isRestoringPurchases = true | ||
SKPaymentQueue.default().restoreCompletedTransactions() | ||
} | ||
|
||
#if DEBUG | ||
func removePurchased(id: String) { | ||
purchased.remove(id) | ||
} | ||
#endif | ||
|
||
//MARK: Products | ||
|
||
enum Products: String, CaseIterable { | ||
case groups = "vc.isv.Tickmate.groups" | ||
|
||
static var allValues: [String] { | ||
allCases.map { $0.rawValue } | ||
} | ||
} | ||
|
||
} | ||
|
||
//MARK: Products Request Delegate | ||
|
||
extension StoreController: SKProductsRequestDelegate { | ||
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { | ||
if !response.invalidProductIdentifiers.isEmpty { | ||
NSLog("Invalid product identifiers found: \(response.invalidProductIdentifiers)") | ||
} | ||
|
||
DispatchQueue.main.async { | ||
if let locale = response.products.first?.priceLocale { | ||
self.priceFormatter.locale = locale | ||
} | ||
withAnimation { | ||
response.products | ||
.map { $0.productIdentifier } | ||
.filter { UserDefaults.standard.bool(forKey: $0) } | ||
.forEach { self.purchased.insert($0) } | ||
self.products = response.products | ||
} | ||
} | ||
} | ||
} | ||
|
||
//MARK: Payment Transaction Observer | ||
|
||
extension StoreController: SKPaymentTransactionObserver { | ||
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { | ||
transactions.forEach { transaction in | ||
let productID = transaction.payment.productIdentifier | ||
|
||
switch transaction.transactionState { | ||
case .purchasing: | ||
withAnimation { | ||
_ = purchasing.insert(productID) | ||
} | ||
case .purchased, .restored: | ||
print("Purchased \(productID)!") | ||
UserDefaults.standard.set(true, forKey: productID) | ||
withAnimation { | ||
purchasing.remove(productID) | ||
purchased.insert(productID) | ||
} | ||
|
||
queue.finishTransaction(transaction) | ||
case .failed, .deferred: | ||
purchasing.remove(productID) | ||
if let error = transaction.error { | ||
print("Purchase of \(productID) failed: \(error)") | ||
} else { | ||
print("Purchase of \(productID) failed.") | ||
} | ||
|
||
queue.finishTransaction(transaction) | ||
@unknown default: | ||
break | ||
} | ||
} | ||
|
||
if isRestoringPurchases { | ||
let restoredProducts = transactions | ||
.filter { $0.transactionState == .restored } | ||
.compactMap { transaction in products.first(where: { $0.productIdentifier == transaction.payment.productIdentifier }) } | ||
if !restoredProducts.isEmpty { | ||
let alertBody = restoredProducts | ||
.map { $0.localizedTitle } | ||
.joined(separator: ", ") | ||
restored = AlertItem(title: "Purchases restored!", message: alertBody) | ||
isRestoringPurchases = false | ||
} | ||
} | ||
} | ||
|
||
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { | ||
// If there were purchases that were restored, isRestoringPurchases, | ||
// would be set to false in paymentQueue(_:, updatedTransactions:), so | ||
// if it's still true here, that means there were no purchases to restore. | ||
if isRestoringPurchases { | ||
restored = AlertItem(title: "No purchases to restore") | ||
isRestoringPurchases = false | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"identifier" : "6A8C1B93", | ||
"nonRenewingSubscriptions" : [ | ||
|
||
], | ||
"products" : [ | ||
{ | ||
"displayPrice" : "1.99", | ||
"familyShareable" : false, | ||
"internalID" : "07E2B154", | ||
"localizations" : [ | ||
{ | ||
"description" : "Unlock track grouping feature", | ||
"displayName" : "Groups", | ||
"locale" : "en_US" | ||
} | ||
], | ||
"productID" : "vc.isv.Tickmate.groups", | ||
"referenceName" : "Groups", | ||
"type" : "NonConsumable" | ||
} | ||
], | ||
"settings" : { | ||
|
||
}, | ||
"subscriptionGroups" : [ | ||
|
||
], | ||
"version" : { | ||
"major" : 1, | ||
"minor" : 1 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// | ||
// AlertItem.swift | ||
// Tickmate | ||
// | ||
// Created by Isaac Lyons on 6/1/21. | ||
// | ||
|
||
import SwiftUI | ||
|
||
class AlertItem: Identifiable { | ||
var title: String | ||
var message: String? | ||
|
||
init(title: String, message: String? = nil) { | ||
self.title = title | ||
self.message = message | ||
} | ||
|
||
var alert: Alert { | ||
if let message = message { | ||
return Alert(title: Text(title), message: Text(message)) | ||
} | ||
|
||
return Alert(title: Text(title)) | ||
} | ||
|
||
static func alert(for alertItem: AlertItem) -> Alert { | ||
alertItem.alert | ||
} | ||
} | ||
|
||
extension View { | ||
func alert(alertItem: Binding<AlertItem?>) -> some View { | ||
self.alert(item: alertItem) { $0.alert } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.