Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The library doesn't return all the necessary info it has and it increases the amount of code a programmer needs to write #185

Closed
4 tasks
gerchicov-bp opened this issue Apr 18, 2017 · 9 comments
Labels
answered Questions which have accepted answers. type: enhancement type: question

Comments

@gerchicov-bp
Copy link

Platform

  • iOS

In app purchase type

  • Auto-Renewable Subscription

Environment

  • Sandbox
  • Production

Version

0.8.4/0.8.5

Report

Issue summary

Some of the methods of this library decrease the amount of returned info which forces to rewrite library methods and/or add extra code and perform extra requests.
In my particular case it is method SwiftyStoreKit.purchaseProduct(...). The intermediate result of this request is SKProduct which has price. But for some purpose you hide it and pass an incomplete info into completion block. What should I do in this case? edit library sources? request all products' info and store it somewhere else? perform additional request to get full product info? It seems You should return SKProduct object instead of your own Product one.

What did you expect to happen

SKProduct object (or dictionary which replaces it) is returned.

What happened instead

Product object is returned which doesn't represent all the info from SKProduct

@bizz84
Copy link
Owner

bizz84 commented Apr 19, 2017

Premise: Two principles behind the design of the SwiftyStoreKit API are:

  • make the API as easy to use as possible
  • expose the smallest amount of information possible, so that it is harder to use it incorrectly.

As these can be in conflict with each other, the best approach is decided on a case-by-case basis.


In your specific case you need to read the price of a SKProduct. Do you need to use this before or after the product is purchased?

If the intent is to show the price to the user before the product is purchased, you can use the retrieveProductsInfo method.

In principle, purchasing a product can be done independently from retrieving product info, even though purchaseProduct calls retrieveProductsInfo internally.

If you are proposing to change the API for this, please explain your use case in detail so I can better understand the requirements.

@bizz84 bizz84 added type: question answered Questions which have accepted answers. labels Apr 19, 2017
@gerchicov-bp
Copy link
Author

gerchicov-bp commented Apr 20, 2017

In my specific case I need to read the price after product purchased to send statistics. I understand that I should receive a product list before showing the pricing page at all.

Anyways I could receive price and price locale after purchase via pure StoreKit and can't do the same thing with this library. And yes - I fixed it for my case - I send additional inner request to get product info by productID after this product is purchased.

@bizz84
Copy link
Owner

bizz84 commented Apr 20, 2017

I see. If you were to use StoreKit only, you would still have to do two steps:

  • Request product info first (to get the SKProduct)
  • Do the purchase for the given SKProduct

I'll treat this as a feature request and see what I can do about it. For now retrieveProductsInfo is the way to go.

@bizz84
Copy link
Owner

bizz84 commented Apr 24, 2017

@gerchicov-bp This is worth a read:
Apple TN2387 In-App Purchase Best Practices

Quoting:

Your app must first send a product request to the App Store before deciding what products to display for purchase in its user interface. Sending a product request allows you to determine whether your products are available for sale in the App Store, thus preventing you from displaying products that cannot be purchased in your app

On this basis, it makes sense to call retrieveProductsInfo first. Once the callback returns, you can store the product price somewhere if needed, and use it again after your purchase is complete.

@bizz84
Copy link
Owner

bizz84 commented May 11, 2017

@gerchicov-bp I tried a few things.

#205 was a tentative solution for this issue, however I didn't like it for a number of reasons:

  • the newPurchase.product value is optional, always present when making a purchase, but never present when restoring purchases or completing transactions:
public struct Purchase {
    public let productId: String
    public let quantity: Int
    public let product: SKProduct? // only available when making a purchase
    public let transaction: PaymentTransaction
    public let originalTransaction: PaymentTransaction?
    public let needsFinishTransaction: Bool
}
  • I think this clutters the API a bit, and you would have to use it like this:
SwiftyStoreKit.purchaseProduct(appBundleId + "." + purchase.rawValue, atomically: true) { result in
    if case .success(let purchase) = result {
        // Deliver content from server, then:
        if purchase.needsFinishTransaction {
            SwiftyStoreKit.finishTransaction(purchase.transaction)
        }
        if let product = purchase.product {
            // do stuff with product
        }
    }
}

A nicer solution would be to leave Purchase as it is, and subclass like this:

struct PurchaseDetails: Purchase {
    public let product: SKProduct
}

This way you always get non-optional access to SKProduct when making a purchase, without cluttering the other APIs.

Unfortunately, subclassing a struct in Swift is not possible and composition is typically the alternative. In this case:

public struct PurchaseDetails {
    public let purchase: Purchase
    public let product: SKProduct
}

This leads me to 59d79b2, which tries to solve the problem with composition, however the resulting API is more clunky as it requires more nesting to get to the information required.

Finally, if I define PurchaseDetails like this:

public struct PurchaseDetails {
    public let productId: String
    public let quantity: Int
    public let product: SKProduct
    public let transaction: PaymentTransaction
    public let originalTransaction: PaymentTransaction?
    public let needsFinishTransaction: Bool
}

I get the benefits of subclassing, and an API that is backwards compatible and nicer to use:

SwiftyStoreKit.purchaseProduct(appBundleId + "." + purchase.rawValue, atomically: true) { result in
    if case .success(let purchase) = result {
        // Deliver content from server, then:
        if purchase.needsFinishTransaction {
            SwiftyStoreKit.finishTransaction(purchase.transaction)
        }
        // do stuff with purchase.product
    }
}

As I said before, it's still possible to get the product from retrieveProductsInfo(), however #206 looks like I have a solution that is simple and backwards compatible. What do you think?

@gerchicov-bp
Copy link
Author

gerchicov-bp commented May 12, 2017

@bizz84
It seems any of your suggested solutions will be ok.

The only possible problem could be for you - SKProduct doesn't support serialization by itself (but it doesn't have influence on this question and answers)

@bizz84
Copy link
Owner

bizz84 commented May 12, 2017

This is now available on release 0.9.1.

@gerchicov-bp
Copy link
Author

@bizz84 I got almost the same issue - I need a detailed product description in SwiftyStoreKit.completeTransactions but currently it returns productId only

@bizz84
Copy link
Owner

bizz84 commented Dec 29, 2017

  • purchaseProduct() enqueues a SKPayment which includes a SKProduct, so this can be returned in the completion block.
  • completeTransactions() only gets updated transactions, which don't contain a SKProduct
  • restorePurchases() is similar to completeTransactions() in this respect

Given a product id, it is always possible to get the corresponding product with a call to retrieveProductsInfo().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
answered Questions which have accepted answers. type: enhancement type: question
Projects
None yet
Development

No branches or pull requests

2 participants