Skip to content

Commit

Permalink
Add restore purchases button
Browse files Browse the repository at this point in the history
Add loading circle when a payment is processing
Show alert when payments are restricted
  • Loading branch information
skjiisa committed Jun 2, 2021
1 parent a7a4257 commit 1b612a6
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 10 deletions.
4 changes: 4 additions & 0 deletions Tickmate/Tickmate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
59F0980225E081CC00667971 /* TicksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F0980125E081CC00667971 /* TicksView.swift */; };
59F3EDBB25FC46770009CDEC /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 59F3EDBA25FC46770009CDEC /* Introspect */; };
59F3EDC225FC4F270009CDEC /* ViewControllerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F3EDC125FC4F270009CDEC /* ViewControllerContainer.swift */; };
59F6170926671C2100CC78E1 /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6170826671C2100CC78E1 /* AlertItem.swift */; };
59F6EA9D25E369830069DF40 /* Tickmate+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EA9C25E369830069DF40 /* Tickmate+Convenience.swift */; };
59F6EAA425E36D510069DF40 /* TickController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EAA325E36D510069DF40 /* TickController.swift */; };
59F6EAA725E36D680069DF40 /* TrackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F6EAA625E36D680069DF40 /* TrackController.swift */; };
Expand Down Expand Up @@ -81,6 +82,7 @@
59EF633625E5DC93003E7259 /* TextWithCaption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithCaption.swift; sourceTree = "<group>"; };
59F0980125E081CC00667971 /* TicksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TicksView.swift; sourceTree = "<group>"; };
59F3EDC125FC4F270009CDEC /* ViewControllerContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerContainer.swift; sourceTree = "<group>"; };
59F6170826671C2100CC78E1 /* AlertItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
59F6EA9C25E369830069DF40 /* Tickmate+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tickmate+Convenience.swift"; sourceTree = "<group>"; };
59F6EAA325E36D510069DF40 /* TickController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TickController.swift; sourceTree = "<group>"; };
59F6EAA625E36D680069DF40 /* TrackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -138,6 +140,7 @@
592CE887264F19B400668684 /* View+Centered.swift */,
5939239B2654CE050085DABD /* PageView.swift */,
5939239D2655B6C90085DABD /* Animation+Push.swift */,
59F6170826671C2100CC78E1 /* AlertItem.swift */,
);
path = "Supplementary Views";
sourceTree = "<group>";
Expand Down Expand Up @@ -330,6 +333,7 @@
59603D452666C85A001482C6 /* StoreController.swift in Sources */,
5991969225F817F7003215CC /* PresetTracks.swift in Sources */,
599D9F5225EC5A4900DF3795 /* TracksView.swift in Sources */,
59F6170926671C2100CC78E1 /* AlertItem.swift in Sources */,
59F6EA9D25E369830069DF40 /* Tickmate+Convenience.swift in Sources */,
59A0D8932645CF1600E01E61 /* View+DismissKeyboard.swift in Sources */,
59EF633725E5DC93003E7259 /* TextWithCaption.swift in Sources */,
Expand Down
58 changes: 51 additions & 7 deletions Tickmate/Tickmate/Controllers/StoreController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ class StoreController: NSObject, ObservableObject {

@Published private(set) var products = [SKProduct]()

@Published var purchased = Set<String>()
@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()
Expand All @@ -32,15 +40,24 @@ class StoreController: NSObject, ObservableObject {
request.start()
}

@discardableResult
func purchase(_ product: SKProduct) -> Bool {
guard SKPaymentQueue.canMakePayments() else { return false }
func purchase(_ product: SKProduct) {
guard isAuthorizedForPayments else { return }

let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
return true
}

func restorePurchases() {
isRestoringPurchases = true
SKPaymentQueue.default().restoreCompletedTransactions()
}

#if DEBUG
func removePurchased(id: String) {
purchased.remove(id)
}
#endif

//MARK: Products

enum Products: String, CaseIterable {
Expand Down Expand Up @@ -85,16 +102,20 @@ extension StoreController: SKPaymentTransactionObserver {

switch transaction.transactionState {
case .purchasing:
break
withAnimation {
_ = purchasing.insert(productID)
}
case .purchased, .restored:
print("Purchased \(productID)!")
UserDefaults.standard.set(true, forKey: productID)
withAnimation {
_ = purchased.insert(productID)
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 {
Expand All @@ -106,5 +127,28 @@ extension StoreController: SKPaymentTransactionObserver {
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
}
}
}
36 changes: 36 additions & 0 deletions Tickmate/Tickmate/Supplementary Views/AlertItem.swift
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 }
}
}
19 changes: 16 additions & 3 deletions Tickmate/Tickmate/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct SettingsView: View {
@Binding var showing: Bool

@State private var timeOffset: Date = Date()
@State private var showingRestrictedPaymentsAlert = false

var body: some View {
Form {
Expand Down Expand Up @@ -72,7 +73,9 @@ struct SettingsView: View {
Section(header: Text("Premium Features"), footer: Text("Groups allow you to swipe left and right between different sets of tracks from the main screen")) {
ForEach(storeController.products, id: \.productIdentifier) { product in
Button {
storeController.purchase(product)
storeController.isAuthorizedForPayments
? storeController.purchase(product)
: (showingRestrictedPaymentsAlert = true)
} label: {
HStack {
TextWithCaption(product.localizedTitle, caption: product.localizedDescription)
Expand All @@ -81,20 +84,30 @@ struct SettingsView: View {
if storeController.purchased.contains(product.productIdentifier) {
Text("Purchased!")
.foregroundColor(.secondary)
} else if storeController.purchasing.contains(product.productIdentifier) {
ProgressView()
} else {
Text(product.price, formatter: storeController.priceFormatter)
.foregroundColor(.accentColor)
.foregroundColor(storeController.isAuthorizedForPayments ? .accentColor : .secondary)
}
}
}
.disabled(storeController.purchased.contains(product.productIdentifier))
}
.alert(isPresented: $showingRestrictedPaymentsAlert) {
Alert(title: Text("Access restricted"), message: Text("You don't have permission to make purchases on this account."))
}

Button("Restore purchases") {
storeController.restorePurchases()
}
.alert(alertItem: $storeController.restored)

#if DEBUG
Button("Reset purchases (debug feature)") {
StoreController.Products.allCases.forEach {
UserDefaults.standard.set(false, forKey: $0.rawValue)
storeController.purchased.remove($0.rawValue)
storeController.removePurchased(id: $0.rawValue)
}
}
#endif
Expand Down

0 comments on commit 1b612a6

Please sign in to comment.