Swiftystorekit: Why you call finishTransaction inside SDK

Created on 20 Oct 2016  路  14Comments  路  Source: bizz84/SwiftyStoreKit

I should manually call finishTransaction after I receive response from my server. If I buy product and my server return 500 error for example SwiftyStoreKit says apple that everything is OK

answered enhancement question

Most helpful comment

I have read that discussion but still don't see why did you decide to call finishTransaction in paymentQueue before app can verify it with server. Could you please explain it?

All 14 comments

This has already been discussed here: https://github.com/bizz84/SwiftyStoreKit/issues/50

I would suggest that you keep track of when a purchase has been completed until you can successfully deliver the content from your server.

I have read that discussion but still don't see why did you decide to call finishTransaction in paymentQueue before app can verify it with server. Could you please explain it?

Ok so to clarify:

Your application should call finishTransaction(_:) only after it has successfully processed the transaction and unlocked the functionality purchased by the user.
  • The purchaseProduct() method was originally conceived for cases where the content can be delivered directly from the app (no server request needed).
  • To better support delivery of content loaded from the server I could do some changes:
public enum PurchaseResult {
     case purchased(productId: String, paymentTransaction: SKPaymentTransaction)
     case success(productId: String)
     case error(error: PurchaseError)
}
/*
 * Purchase a product. Usage:
 * Use atomically = true for content that can be delivered from the app.
 * This will result in .success(productId) if the purchase is successful.
 * Use atomically = false for content that will be delivered by a server. 
 * This will result in .purchased(productId, paymentTransaction) if the purchase is successful.
 * Clients are responsible for keeping track of the paymentTransaction until the content is delivered, and must call finishTransaction() to clear the purchase from the transaction queue and finalise the purchase.
 */
func purchaseProduct(productId: String, atomically: Bool = true, completion: (PurchaseResult) -> ())
func finishTransaction(paymentTransaction: SKPaymentTransaction)

Some other considerations

If a clients starts a non atomic purchase and quits the app before delivering the content from the server, the completeTransactions() API (to be used when the app starts) also calls finishTransaction() by default.

Maybe it would make sense to also do this?

func completeTransactions(atomically: Bool = true, _ completion: ([CompletedTransaction]) -> ())

I would then assume that if the client says completeTransactions(atomically: false) then it has the responsibility of calling finishTransaction() on each transaction.

@Stealth2012 Would these proposed API changes meet your requirements?

Don't know for @Stealth2012, but I think that would be a great move 馃憤

Ok cool. I'll start working on this. Given my current timescales I think it will take me a week to get this done.

Yeah, looks great!

I'm not sure about what is the most desirable API for this. _Some feedback is welcome_.

First of all, I have updated the PurchaseResult type returned by SwiftyStoreKit.purchaseProduct() to look like this:

public enum PurchaseResult {
    case success(productId: String, transaction: SKPaymentTransaction?)
    case error(error: PurchaseError)
}
func purchaseProduct(productId: String, atomically: Bool = true, completion: (PurchaseResult) -> ())

If the user calls purchaseProduct() with atomically = false, she will get a non-nil transaction. After the content is delivered from the server, the transaction can be used like this:

SwiftyStoreKit.finishTransaction(transaction)

However, I'm not sure if the whole SKPaymentTransaction object should be exposed here as it has a lot of properties and would like to prevent the user from using this incorrectly.

After all, SwiftyStoreKit should take care of the low level details of working with transactions.

With this in mind, I was thinking to declare a phantom protocol and attach this to an extension of SKPaymentTransaction:

protocol PaymentTransaction { }
extension SKPaymentTransaction: PaymentTransaction { }

The PurchaseResult type would then look like this:

public enum PurchaseResult {
    case success(productId: String, transaction: PaymentTransaction?)
    case error(error: PurchaseError)
}

Finishing the transaction would then be delegated to SwiftyStoreKit:

func finishTransaction(transaction: PaymentTransaction) {
   guard let skTransaction = transaction as? SKPaymentTransaction else {
      print("Object is not a SKPaymentTransaction: \(transaction)
      return
   }
   SKPaymentQueue.default().finishTransaction(skTransaction)
}

I believe that a stricter API makes SwiftyStoreKit safer to use.

@tbaranes @Stealth2012 What are your thoughts?

That's a good question. Both have pro and cons, but since SwiftyStoreKit is abstracting all the StoreKit symbols, I'll go for a stricter API.

Also, if necessary, PaymentTransaction can evolve in the future by exposing some of SKPaymentTransaction properties to fulfil the server requirements.

I agree, but would be nice if PaymentTransaction could expose transactionId from SKPaymentTransaction.

This feature is now dev complete: https://github.com/bizz84/SwiftyStoreKit/pull/107/files

The API has changed slightly from the initial proposal so that completeTransactions(), purchaseProduct() and restoreProducts() all return the same Product type on success:

// Purchased or restored product
public struct Product {
    public let productId: String
    public let transaction: PaymentTransaction
    public let needsFinishTransaction: Bool
}

@Stealth2012 do you want to expose SKPaymentTransaction.transactionIdentifer? What would be the use case for this?

@bizz84 we use server to validate purchase and we need transactionIdentifier to find correct transaction in app receipt

Non-atomic purchases are now implemented and available on pod version 0.6.0.

If there are any problems with the new API, feel free to re-open this issue.

Hi, I found that there is still a case when finishTransaction will be called inside SwiftyStoreKit.
This is the code from paymentQueue:
```
if transactionState != .purchasing {

            let product = Product(productId: transaction.payment.productIdentifier, transaction: transaction, needsFinishTransaction: !atomically)

            completedTransactions.append(product)

            print("Finishing transaction for payment \"\(transaction.payment.productIdentifier)\" with state: \(transactionState.stringValue)")

            paymentQueue.finishTransaction(transaction)
        }

Looks like there is missing a check if `atomically` is `true`:

if atomically {
paymentQueue.finishTransaction(transaction)
}
```

@Stealth2012 Thanks for spotting this!

I have fixed it in pod version 0.6.1.

Changeset: https://github.com/bizz84/SwiftyStoreKit/commit/0061034e4cc7440ecaeccf8991c75e3b7dfd276c

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lsamaria picture lsamaria  路  6Comments

mickyzinho picture mickyzinho  路  3Comments

jlaws picture jlaws  路  6Comments

astrokin picture astrokin  路  4Comments

JonathanDowning picture JonathanDowning  路  7Comments