Skip to content

Instantly share code, notes, and snippets.

@kimyongin
Created August 23, 2018 08:14
Show Gist options
  • Save kimyongin/59dab8591968d7a69b1dacb1ff1a01c9 to your computer and use it in GitHub Desktop.
Save kimyongin/59dab8591968d7a69b1dacb1ff1a01c9 to your computer and use it in GitHub Desktop.
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)
}
}
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)
}
}
}
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