As a programmer, aren’t you suspicious when you write something that works
flawlessly the first time? I worry it just means there’s a disastrous time
bomb.

In Apple’s documentation about in-app purchases, there’s a lot of talk
about verifying receipts, but when I look at articles and sample code, it
seems payment transactions are all anyone really cares about. And if that’s
so, it seems there are two parts to an in-app store: (1) a “clerk”: code
with no user interface that runs practically all the time to deal with
payment transactions; and (2) a flashy “salesman” with a UI to display
products, make the pitch, and offer buttons the user can click to make the
clerk spring into action.

With that in mind, I decided to write a single-file, general-purpose clerk
that I could use in any app. It seems to work beautifully, but one thing
surprised me: when I deleted my app, reloaded, and restored purchases, the
payment queue replayed instantly, without asking me to sign in. It went so
smoothly that I’m suspicious.

So I’m going to share the clerk code with you here and ask for critiques,
if you have time. Thanks!

//
//  InAppStore.swift
//
//  Created by Charles Jenkins on 12/25/16.
//  Offered into the public domain.
//

import StoreKit

// The store user interface implements this protocol
// in order to be notified of updates as the payment
// queue is processed. It should plug itself into
// InAppStore.instance.userInterface when it appears
// and clear the same when it disappears; and should
// react to update() callbacks by displaying results
// reported by InAppStore.instance.latestTransaction.

protocol InAppStoreUserInterfaceProvider {
  func update()
}

// The app delegate should implement this protocol
// and be able to react to activate() callbacks
// regardless of whether any user interface is on
// display.

protocol InAppStoreProductActivator {
  func activate( transaction: SKPaymentTransaction )
}

// Store operations management class

final class InAppStore :
  NSObject,
  SKPaymentTransactionObserver
{

  // MARK: - Public Properties

  static let instance = InAppStore()

  // This object's lifetime is far greater than the
  // store's UI, so we need to handle all queued
  // responses without depending on the store's UI.
  // The userInterface variable allows the store UI
  // to temporarily plug in and sign up for update
  // callbacks.

  var userInterface: InAppStoreUserInterfaceProvider?

  // When the store UI appears or receives update()
  // callbacks, it can check the latestTransaction
  // variable to determine which elements or messages
  // to display.

  var latestTransaction: SKPaymentTransaction? {
    get {
      forgetExpiredDeferral()
      return _latestTransaction
    }
    set {
      _latestTransaction = newValue
    }
  }

  // Ask if user can make a purchase before calling makePurchase()

  var canMakePurchase: Bool {
    return SKPaymentQueue.canMakePayments()
  }

  // MARK: - Non-Public Properties

  private var _latestTransaction: SKPaymentTransaction?

  private var productActivator: InAppStoreProductActivator?

  private var queue : SKPaymentQueue {
    return SKPaymentQueue.default()
  }

  // MARK: - Public Methods

  // Call this function as soon as possible after
  // launching the app, so waiting transactions
  // can be dealt with immediately.
  //
  // NOTE: I do this in the app delegate's
  // application( _: didFinishLaunchingWithOptions ),
  // but first I check user defaults: if all products
  // have already been purchased, we'll never show
  // the store and we don't need to startObserving()

  func startObserving( productActivator: InAppStoreProductActivator )
  {
    NSLog( "Adding payment queue observer" )
    self.productActivator = productActivator
    queue.add( self )
  }

  func stopObserving()
  {
    NSLog( "Removing payment queue observer" )
    queue.remove( self )
    self.productActivator = nil
  }

  static public func requestActiveProducts(
    productIds: [String],
    observer: SKProductsRequestDelegate
  ) {
    let req = SKProductsRequest(
      productIdentifiers: Set<String>( productIds )
    )
    req.delegate = observer
    req.start()
  }

  func restorePurchases()
  {
    // New successful transactions will be sent to the
    // payment queue to mimic all previously completed
    // successful transactions, which should trigger
    // the proper product activations
    NSLog( "Restore Purchases - Requesting completed transactions" )
    self.queue.restoreCompletedTransactions()
  }

  func makePurchase( paymentRequest: SKPayment )
  {
    queue.add( paymentRequest )
  }

  // MARK: - Non-Public Methods

  private func forgetExpiredDeferral()
  {
    if
      let tran = _latestTransaction,
      let date = tran.transactionDate,
      tran.transactionState == .deferred
    {
      let timePassed = -( date.timeIntervalSinceNow )
      let oneDayInSeconds = 60 * 60 * 24
      if timePassed > TimeInterval( oneDayInSeconds ) {
        _latestTransaction = nil
      }
    }
  }

  func paymentQueue(
    _ queue: SKPaymentQueue,
    updatedTransactions transactions: [SKPaymentTransaction]
  ) {
    for tran in transactions {

      latestTransaction = tran

      let state = tran.transactionState

      if state == .purchased {
        NSLog( "Purchase succeeded: Activating" )
        productActivator?.activate( transaction: tran )
      }

      if state == .restored {
        NSLog( "Purchase restored: Activating" )
        productActivator?.activate( transaction: tran )
      }

      if state != .purchasing {
        queue.finishTransaction( tran )
      }

      if let userInterface = userInterface {
        userInterface.update()
      }

    }
  }

  deinit {
    stopObserving()
  }

}


-- 

Charles
_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to