title: iOS in-app purchases randomly not being recognized, or never recognized to begin with
body: <p>Ok this one really has me stumped and if anyone can figure it out I will love you long time. I have several iOS apps and I'm getting a lot of reports from users that when they pay using apple's in-app payment system to purchase app upgrades, the apps are not recognizing their purchase, or sometimes it recognizes it for a day and then it stops working.</p>
  
  <p>The weird thing is I have tested it on my iPhone and iPad, both in sandbox and after publishing with a real account, and I never have these issues. I thought maybe it was an issue with the new iOS 11 but even users on iOS 10 and 9 are reporting this issue. For some users, uninstalling the apps and downloading again from app store, then clicking restore purchases button fixes it, but for most user's it doesn't.</p>
  
  <p>I have tried everything apple recommends, such as telling users to make sure they are using the correct apple ID, making sure the device is set to allows in-app payments, etc but none of those solutions apply or work.</p>
  
  <p>So without further ado, here's my code</p>
  
  <p>SharedDefaults</p>
  
  <pre><code>fileprivate enum SUDK: String {
  case updatedToProVersion = "UDK_UPDATED_TO_PRO_VERSION"
  }
  
  public class SharedDefaults: NSObject {
  public static var shared: SharedDefaults!
  
  private let defaults: UserDefaults
  
  public init(groupIdentifier: String) {
      if let userDefaults = UserDefaults(suiteName: groupIdentifier) {
          defaults = userDefaults
      } else {
          defaults = UserDefaults.standard
          assert(false, "Defaults could not be created with the given app         group - \(groupIdentifier)")
      }
  
      super.init()
  }
  
  public static var isPremiumVersionActive: Bool {
      set {
          shared.defaults.set(newValue, forKey:  SUDK.updatedToProVersion.rawValue)
      }
      get {
          //// return true to set pro version to always on. This is the in-app purchase the user can make to upgrade the app.
          ////return true
          return shared.defaults.bool(forKey: SUDK.updatedToProVersion.rawValue)
      }
  }
  </code></pre>
  
  <p>Upgrade Screen and Purchase Handlers</p>
  
  <pre><code>import UIKit
  import StoreKit
  
  class UpgradeNowController: UITableViewController {
  //MARK: Constants
  struct CellIdentifiers {
      static let buttonCell = "UpgradeNowButtonCell"
      static let discountedButtonCell = "UpgradeNowDiscountedButtonCell"
      static let buttonOnlyCell = "UpgradeNowButtonOnlyCell"
      static let detailCell = "UpgradeNowDetailCell"
  }
  
  
  //MARK: Variables
  var allProducts = [SKProduct]()
  var products = [SKProduct]()
  public var activeProductID: String!
  var detailItems: [(title: String, detail: String, image: String?)] = []
  
  //MARK: Lifecycle
  override func viewDidLoad() {
      super.viewDidLoad()
  
      activeProductID = RageProducts.proversionupgrade
  
      refreshControl = UIRefreshControl()
      refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
  
      let restoreButton = UIBarButtonItem(title: "Restore",
                                          style: .plain,
                                          target: self,
                                          action: #selector(UpgradeNowController.restoreTapped(_:)))
      navigationItem.rightBarButtonItem = restoreButton
  
      NotificationCenter.default.addObserver(self, selector: #selector(UpgradeNowController.handlePurchaseNotification(_:)),
                                             name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification),
                                             object: nil)
  
      configureTableView()
      reloadTableView()
      configureDetailItems()
  }
  
  deinit {
      NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: nil)
  }
  
  
  func reload() {
      products = []
  
      tableView.reloadData()
  
      RageProducts.store.requestProducts{ [weak self] success, products in
          if success {
              self?.allProducts = products!
              self?.products = products!.filter { $0.productIdentifier == self?.activeProductID }
              self?.tableView.reloadData()
          }
  
          self?.refreshControl?.endRefreshing()
      }
  }
  
  func restoreTapped(_ sender: AnyObject) {
      RageProducts.store.restorePurchases()
  }
  
  func handlePurchaseNotification(_ notification: Notification) {
      guard let productID = notification.object as? String else { return }
  
      for (index, product) in products.enumerated() {
          guard product.productIdentifier == productID else { continue }
  
          tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .fade)
      }
  
      SharedDefaults.isPremiumVersionActive = true
  }
  
  
  
  
  //MARK: IAPPopupViewControllerDelegate
  extension UpgradeNowController: IAPPopupViewControllerDelegate {
  
  func iapPopupViewController(_ viewController: IAPPopupViewController,  didTapCancelForProduct product: SKProduct) {
      viewController.dismiss(animated: true)
  }
  
  func iapPopupViewController(_ viewController: IAPPopupViewController, didTapPurchaseForProduct product: SKProduct) {
      viewController.dismiss(animated: true)
      RageProducts.store.buyProduct(product)
  }
  
  }
  </code></pre>
  
  <p>IAPHelper</p>
  
  <pre><code>import StoreKit
  
  public typealias ProductIdentifier = String
  public typealias ProductsRequestCompletionHandler = (_ success: Bool, _ products: [SKProduct]?) -&gt; ()
  
  open class IAPHelper : NSObject  {
  
    static let IAPHelperPurchaseNotification = "IAPHelperPurchaseNotification"
      fileprivate let productIdentifiers: Set&lt;ProductIdentifier&gt;
    fileprivate var purchasedProductIdentifiers = Set&lt;ProductIdentifier&gt;()
    fileprivate var productsRequest: SKProductsRequest?
    fileprivate var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
  
    public init(productIds: Set&lt;ProductIdentifier&gt;) {
  productIdentifiers = productIds
  for productIdentifier in productIds {
    let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
    if purchased {
      purchasedProductIdentifiers.insert(productIdentifier)
      print("Previously purchased: \(productIdentifier)")
    } else {
      print("Not purchased: \(productIdentifier)")
    }
  }
  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) {
  print("Buying \(product.productIdentifier)...")
  let payment = SKPayment(product: product)
  SKPaymentQueue.default().add(payment)
    }
  
    public func isProductPurchased(_ productIdentifier: ProductIdentifier) -&gt; Bool {
  return purchasedProductIdentifiers.contains(productIdentifier)
   }
  
   public class func canMakePayments() -&gt; Bool {
     return SKPaymentQueue.canMakePayments()
   }
  
    public func restorePurchases() {
  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 p in products {
    print("Found product: \(p.productIdentifier) \(p.localizedTitle)    \(p.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: \(String(describing: transaction.error?.localizedDescription))")
    }
  }
  
  SKPaymentQueue.default().finishTransaction(transaction)
   }
  
  private func deliverPurchaseNotificationFor(identifier: String?) {
  guard let identifier = identifier else { return }
  
  purchasedProductIdentifiers.insert(identifier)
  UserDefaults.standard.set(true, forKey: identifier)
  UserDefaults.standard.synchronize()
  NotificationCenter.default.post(name: NSNotification.Name(rawValue: IAPHelper.IAPHelperPurchaseNotification), object: identifier)
   }
  }
  </code></pre>
  
  <p>Note: lots of code has been stripped I've only included relevant code to the in app purchases. Maybe someone can look at it and let me know if I'm doing anything wrong but as far as I can see and research I've followed all guidelines for storekit and in app purchases as best as possible.</p>