Created
August 23, 2018 08:14
-
-
Save kimyongin/59dab8591968d7a69b1dacb1ff1a01c9 to your computer and use it in GitHub Desktop.
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
import StoreKit | |
public typealias ProductIdentifier = String | |
public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -> () | |
open class IAPHelper: NSObject { | |
// MARK: - Properties | |
fileprivate let productIdentifiers: Set<ProductIdentifier> | |
public var purchasedProducts = Set<ProductIdentifier>() | |
fileprivate var productsRequest: SKProductsRequest? | |
fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler? | |
// MARK: - Initializers | |
public init(productIds: Set<ProductIdentifier>) { | |
productIdentifiers = productIds | |
purchasedProducts = Set(productIds.filter { UserDefaults.standard.bool(forKey: $0) }) | |
super.init() | |
SKPaymentQueue.default().add(self) | |
} | |
} | |
// MARK: - StoreKit API | |
extension IAPHelper { | |
public func requestProducts(completionHandler: @escaping ProductsRequestCompletionHandler) { | |
productsRequest?.cancel() | |
productsRequestCompletionHandler = completionHandler | |
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers) | |
productsRequest!.delegate = self | |
productsRequest!.start() | |
} | |
public func buyProduct(_ product: SKProduct) { | |
let payment = SKPayment(product: product) | |
SKPaymentQueue.default().add(payment) | |
} | |
public func isPurchased(_ productIdentifier: ProductIdentifier) -> Bool { | |
return purchasedProducts.contains(productIdentifier) | |
} | |
public class func canMakePayments() -> Bool { | |
return SKPaymentQueue.canMakePayments() | |
} | |
public func restorePurchases() { | |
// Restore Consumables and Non-Consumables from Apple | |
SKPaymentQueue.default().restoreCompletedTransactions() | |
} | |
} | |
// MARK: - SKProductsRequestDelegate | |
extension IAPHelper: SKProductsRequestDelegate { | |
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { | |
let products = response.products | |
print("Loaded list of products...") | |
productsRequestCompletionHandler?(true, products) | |
clearRequestAndHandler() | |
for prod in products { | |
print("Found product: \(prod.productIdentifier) \(prod.localizedTitle) \(prod.price.floatValue)") | |
} | |
} | |
public func request(_ request: SKRequest, didFailWithError error: Error) { | |
print("Failed to load list of products.") | |
print("Error: \(error.localizedDescription)") | |
productsRequestCompletionHandler?(false, nil) | |
clearRequestAndHandler() | |
} | |
private func clearRequestAndHandler() { | |
productsRequest = nil | |
productsRequestCompletionHandler = nil | |
} | |
} | |
// MARK: - SKPaymentTransactionObserver | |
extension IAPHelper: SKPaymentTransactionObserver { | |
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { | |
for transaction in transactions { | |
switch (transaction.transactionState) { | |
case .purchased: | |
complete(transaction: transaction) | |
break | |
case .failed: | |
fail(transaction: transaction) | |
break | |
case .restored: | |
restore(transaction: transaction) | |
break | |
case .deferred: | |
break | |
case .purchasing: | |
break | |
} | |
} | |
} | |
private func complete(transaction: SKPaymentTransaction) { | |
print("complete...") | |
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier) | |
SKPaymentQueue.default().finishTransaction(transaction) | |
} | |
private func restore(transaction: SKPaymentTransaction) { | |
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return } | |
print("restore... \(productIdentifier)") | |
deliverPurchaseNotificationFor(identifier: productIdentifier) | |
SKPaymentQueue.default().finishTransaction(transaction) | |
} | |
private func fail(transaction: SKPaymentTransaction) { | |
print("fail...") | |
if let transactionError = transaction.error as? NSError { | |
if transactionError.code != SKError.paymentCancelled.rawValue { | |
print("Transaction Error: \(transaction.error?.localizedDescription)") | |
} | |
} | |
SKPaymentQueue.default().finishTransaction(transaction) | |
} | |
private func deliverPurchaseNotificationFor(identifier: String?) { | |
guard let identifier = identifier else { return } | |
OwlProducts.handlePurchase(productID: identifier) | |
} | |
} |
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
import Foundation | |
import Parse | |
public struct OwlProducts { | |
// MARK: - Properties | |
static let PurchaseNotification = "OwlProductsPurchaseNotification" | |
static let randomProductID = "com.back40.InsomniOwl.RandomOwls" | |
static let productIDsConsumables: Set<ProductIdentifier> = [randomProductID] | |
static let productIDsNonConsumables: Set<ProductIdentifier> = [ | |
"com.back40.InsomniOwl.CarefreeOwl", | |
"com.back40.InsomniOwl.GoodJobOwl", | |
"com.back40.InsomniOwl.CouchOwl", | |
"com.back40.InsomniOwl.NightOwl", | |
"com.back40.InsomniOwl.LonelyOwl", | |
"com.back40.InsomniOwl.ShyOwl", | |
"com.back40.InsomniOwl.CryingOwl", | |
"com.back40.InsomniOwl.GoodNightOwl", | |
"com.back40.InsomniOwl.InLoveOwl"] | |
static let productIDsNonRenewing: Set<ProductIdentifier> = ["com.back40.InsomniOwl.3monthsOfRandom", | |
"com.back40.InsomniOwl.6monthsOfRandom"] | |
static let randomImages = [ | |
UIImage(named: "CarefreeOwl"), | |
UIImage(named: "GoodJobOwl"), | |
UIImage(named: "CouchOwl"), | |
UIImage(named: "NightOwl"), | |
UIImage(named: "LonelyOwl"), | |
UIImage(named: "ShyOwl"), | |
UIImage(named: "CryingOwl"), | |
UIImage(named: "GoodNightOwl"), | |
UIImage(named: "InLoveOwl") | |
] | |
public static let store = IAPHelper(productIds: OwlProducts.productIDsConsumables | |
.union(OwlProducts.productIDsNonConsumables) | |
.union(OwlProducts.productIDsNonRenewing)) | |
public static func resourceName(for productIdentifier: String) -> String? { | |
return productIdentifier.components(separatedBy: ".").last | |
} | |
public static func clearProducts() { | |
store.purchasedProducts.removeAll() | |
} | |
public static func handlePurchase(productID: String) { | |
if productIDsConsumables.contains(productID) { | |
UserSettings.shared.increaseRandomRemaining(by: 5) | |
setRandomProduct(with: true) | |
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification), object: nil) | |
} else if productIDsNonRenewing.contains(productID), productID.contains("3months") { | |
handleMonthlySubscription(months: 3) | |
} else if productIDsNonRenewing.contains(productID), productID.contains("6months") { | |
handleMonthlySubscription(months: 6) | |
} else if productIDsNonConsumables.contains(productID) { | |
UserDefaults.standard.set(true, forKey: productID) | |
store.purchasedProducts.insert(productID) | |
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification), object: nil) | |
} | |
} | |
public static func setRandomProduct(with paidUp: Bool) { | |
if paidUp { | |
UserDefaults.standard.set(true, forKey: OwlProducts.randomProductID) | |
store.purchasedProducts.insert(OwlProducts.randomProductID) | |
} else { | |
UserDefaults.standard.set(false, forKey: OwlProducts.randomProductID) | |
store.purchasedProducts.remove(OwlProducts.randomProductID) | |
} | |
} | |
public static func daysRemainingOnSubscription() -> Int { | |
if let expiryDate = UserSettings.shared.expirationDate { | |
return Calendar.current.dateComponents([.day], from: Date(), to: expiryDate).day! | |
} | |
return 0 | |
} | |
public static func getExpiryDateString() -> String { | |
let remaining = daysRemainingOnSubscription() | |
if remaining > 0, let expiryDate = UserSettings.shared.expirationDate { | |
let dateFormatter = DateFormatter() | |
dateFormatter.dateFormat = "dd/MM/yyyy" | |
return "Subscribed! \nExpires: \(dateFormatter.string(from: expiryDate)) (\(remaining) Days)" | |
} | |
return "Not Subscribed" | |
} | |
public static func paidUp() -> Bool { | |
var paidUp = false | |
if OwlProducts.daysRemainingOnSubscription() > 0 { | |
paidUp = true | |
} else if UserSettings.shared.randomRemaining > 0 { | |
paidUp = true | |
} | |
setRandomProduct(with: paidUp) | |
return paidUp | |
} | |
public static func syncExpiration(local: Date?, completion: @escaping (_ object: PFObject?) -> ()) { | |
// Query Parse for expiration date. | |
guard let user = PFUser.current(), | |
let userID = user.objectId, | |
user.isAuthenticated else { | |
return | |
} | |
let query = PFQuery(className: "_User") | |
query.getObjectInBackground(withId: userID) { | |
object, error in | |
let parseExpiration = object?[expirationDateKey] as? Date | |
// Get to latest date between Parse and local. | |
var latestDate: Date? | |
if parseExpiration == nil { | |
latestDate = local | |
} else if local == nil { | |
latestDate = parseExpiration | |
} else if parseExpiration!.compare(local!) == .orderedDescending { | |
latestDate = parseExpiration | |
} else { | |
latestDate = local | |
} | |
if let latestDate = latestDate { | |
// Update local | |
UserSettings.shared.expirationDate = latestDate | |
// See if subscription valid | |
if latestDate.compare(Date()) == .orderedDescending { | |
setRandomProduct(with: true) | |
} | |
} | |
completion(object) | |
} | |
} | |
private static func handleMonthlySubscription(months: Int) { | |
// Update local and Parse with new subscription. | |
syncExpiration(local: UserSettings.shared.expirationDate) { | |
object in | |
// Increase local | |
UserSettings.shared.increaseRandomExpirationDate(by: months) | |
setRandomProduct(with: true) | |
// Update Parse with extended purchase | |
object?[expirationDateKey] = UserSettings.shared.expirationDate | |
object?.saveInBackground() | |
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification), object: nil) | |
} | |
} | |
} |
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
import Foundation | |
let expirationDateKey = "ExpirationDate" | |
class UserSettings { | |
// MARK: - Properties | |
static let shared = UserSettings() | |
init() { | |
} | |
public var expirationDate: Date? { | |
set { | |
UserDefaults.standard.set(newValue, forKey: expirationDateKey) | |
} | |
get { | |
return UserDefaults.standard.object(forKey: expirationDateKey) as? Date | |
} | |
} | |
public var randomRemaining: Int { | |
set { | |
UserDefaults.standard.set(newValue, forKey: "remaining") | |
} | |
get { | |
return UserDefaults.standard.integer(forKey: "remaining") | |
} | |
} | |
public var lastRandomIndex: Int { | |
set { | |
UserDefaults.standard.set(newValue, forKey: "lastRandomIndex") | |
} | |
get { | |
return UserDefaults.standard.integer(forKey: "lastRandomIndex") | |
} | |
} | |
public func increaseRandomExpirationDate(by months: Int) { | |
let lastDate = expirationDate ?? Date() | |
let newDate = Calendar.current.date(byAdding: .month, value: months, to: lastDate) | |
expirationDate = newDate | |
} | |
public func increaseRandomRemaining(by times: Int) { | |
let lastTimes = (randomRemaining < 0) ? 0 : randomRemaining | |
randomRemaining = lastTimes + times | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment