And of course the second after I posted that, I decided userInterface should be a weak var.
On Sun, Jan 8, 2017 at 8:29 AM, Charles Jenkins <cejw...@gmail.com> wrote: > 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 > -- 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