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

PaymentQueueController's shouldAddStorePayment method Implementation #240

Closed
zntfdr opened this issue Jul 3, 2017 · 45 comments
Closed

PaymentQueueController's shouldAddStorePayment method Implementation #240

zntfdr opened this issue Jul 3, 2017 · 45 comments
Labels
area: purchase flows purchase processes, efficiency and failures help wanted iOS & iPadOS directly related to iPhone and iPad type: enhancement

Comments

@zntfdr
Copy link

zntfdr commented Jul 3, 2017

Platform

  • iOS

Version

ℹ swift-4.0 branch

Report

Issue summary

ℹ As of iOS 11, we have a new optional paymentQueue method that SwiftyStoreKit's PaymentQueueController can implement.

What did you expect to happen

ℹ As far as I can tell, PaymentQueueController is an internal class (not accessible from outside) therefore there's no way to implement this in our app.

What happened instead

ℹ I simply added this code inside SwiftyStoreKit/PaymentQueueController.swift:

  @available(iOS 11.0, *)
  func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
    return true
  }

Question

Is there a better way to do this? What I did above works as long as I don't update the pods: then I will have to do it again (for each $ pod update).

@bizz84
Copy link
Owner

bizz84 commented Jul 6, 2017

@zntfdr thanks for reporting this.

Note that this method is optional:

@available(iOS 11.0, *)
optional public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool

From Apple Docs:

This delegate method is called when the user has started an in-app purchase in the App Store, and is continuing the transaction in your app. Specifically, if your app is already installed, the method is called automatically.
If your app is not yet installed when the user starts the in-app purchase in the App Store, the user gets a notification when the app installation is complete. This method is called when the user taps the notification. Otherwise, if the user opens the app manually, this method is called only if the app is opened soon after the purchase was started.

We need to verify that if this method is implemented and returns true, then this delegate method in the payment queue is called:

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])

If this is the case, SwiftyStoreKit will then handle this by calling the completion block of completeTransactions().

Questions

  • Should SwiftyStoreKit implement this method to always return true?
  • Should the resulting purchase be handled by completeTransactions()?
  • How can we test that this works correctly when the purchase is initiated in the App Store? Seems like this would be hard to test with the Sandbox.
  • Do we want to distinguish if a payment was started within the app or from the App Store? Potentially this could be done by adding a Purchase.isAppStoreInitiated value:
public struct Purchase {
    public let productId: String
    public let quantity: Int
    public let transaction: PaymentTransaction
    public let originalTransaction: PaymentTransaction?
    public let needsFinishTransaction: Bool
    public let isAppStoreInitiated: Bool
}

@bizz84 bizz84 added type: enhancement area: purchase flows purchase processes, efficiency and failures iOS & iPadOS directly related to iPhone and iPad labels Jul 6, 2017
@zntfdr
Copy link
Author

zntfdr commented Jul 6, 2017

Hi Andrea,
thanks for the reply.

You're correct: it just calls the method you mentioned.

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])

Regarding the other questions:

  • Should SwiftyStoreKit implement this method to always return true?
    Absolutely not: the user might try to buy something that is not available to him already (for example because he doesn't have an account yet, or he hasn't reached the right game level etc).
  • Should the resulting purchase be handled by completeTransactions()?
    I believe this is the correct way (and it does this already automatically).
  • How can we test that this works correctly when the purchase is initiated in the App Store? Seems like this would be hard to test with the Sandbox.
    We simply open the following link in our device:
    itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name.
    I've already tested it and it works as expected.
    You can find more information on the WWDC17 session What's New in StoreKit.
    (slide number 165 shows the link above)
  • Do we want to distinguish if a payment was started within the app or from the App Store?
    I haven't checked but I believe it's not possible to know where the the purchase comes from.
    On the same matter: can we detect whether the user has actually purchased or redeemed any of our product? If the answer is no, then I believe the answer is no for this as well.
    Either way, I don't see an actual benefit in doing this.

@bizz84
Copy link
Owner

bizz84 commented Jul 6, 2017

Should SwiftyStoreKit implement this method to always return true?
Absolutely not: the user might try to buy something that is not available to him already (for example because he doesn't have an account yet, or he hasn't reached the right game level etc).

Sounds like the user should be able to register a closure to be called by SwiftyStoreKit when shouldAddStorePayment is called. Something like this would work:

var shouldAddStorePaymentHandler: (_ payment: SKPayment, _ product: SKProduct) -> Bool

I'd like to open this for contributions from the community. This is an outline that may be useful to get things started:

Summary of requirements

  1. Add a closure to SwiftyStoreKit. Sample implementation:
class SwiftyStoreKit {
     typealias ShouldAddStorePaymentHandler = (_ payment: SKPayment, _ product: SKProduct) -> Bool
     class var shouldAddStorePaymentHandler: ShouldAddStorePaymentHandler? {
        didSet {
            sharedInstance.paymentQueueController.shouldAddStorePaymentHandler = shouldAddStorePaymentHandler
        }
     }
}
  1. Implement shouldAddStorePayment method:
public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
    return shouldAddStorePaymentHandler?(payment, product) ?? false
}
  1. Unit tests
  2. Verify app compiles and runs on tvOS, macOS where this is not available
  3. Update README and CHANGELOG
  4. Pull request to swift-4.0

@tomthecarrot
Copy link

Was the swift-4.0 branch merged or transferred somewhere else? It appears to have been deleted from this repo and the SwiftyStoreKit pod build is currently failing on Xcode 9 / Swift 4.

@bizz84
Copy link
Owner

bizz84 commented Jul 16, 2017

I deleted it because the project now compiles fine on Xcode 9 beta 3. You can point your pod to develop until I publish a new pod version.

@Esqarrouth
Copy link

Hey, whats the expected time for this feature to be ready?

Also a concern I have after watching the WWDC video:

I think App Store IAP is looking if SKPaymentTransactionObserver is implemented in AppDelegate. If it is, its showing the IAPs that can be bought from the store, if not I think its hiding those IAPs. Of course I have no way of testing or confirming this at the moment but something to keep on mind.

http://prntscr.com/fxnfbx

@zntfdr
Copy link
Author

zntfdr commented Jul 19, 2017

Also a concern I have after watching the WWDC video:
I think App Store IAP is looking if SKPaymentTransactionObserver is implemented in AppDelegate.

Don't mind that, it's just for simplicity and demo's sake.
I'm already using this feature as I've described above, with the AppDelegate not being any kind of StoreKit delegate. 😉

Hey, whats the expected time for this feature to be ready?

This is an open source project! 😀 If you'd like, please feel free to implement it and send a PR 😊

@Esqarrouth
Copy link

Thanks for the answer.

I'd love to contribute but I don't understand Apple's crappy StoreKit frameworks to implement them in anyway, thats why I'm using this great library 😄

@fishcharlie
Copy link
Contributor

@zntfdr @bizz84 Any updates on this?

@bizz84
Copy link
Owner

bizz84 commented Aug 13, 2017

Looks like no willing contributors are to be seen on this one. ;)

I’ll try to implement it this week.

@fishcharlie
Copy link
Contributor

@bizz84 Awesome! I would help but haven't really worked with StoreKit on a low level in a while. Especially because of this package. Thanks for this amazing package!!

@bizz84
Copy link
Owner

bizz84 commented Aug 21, 2017

This has now been implemented and is available on version 0.10.6.

I have not tested it on device - please let me know if it works ok for you.

Once this is confirmed, I will close the issue.

@zntfdr
Copy link
Author

zntfdr commented Aug 21, 2017

Great, thanks!
I will test it this evening: looking at the code everything seems all right :)

Follow up: works for me! 💯 💯 Thanks 🤗

@bizz84
Copy link
Owner

bizz84 commented Aug 21, 2017

Awesome!

@bizz84 bizz84 closed this as completed Aug 21, 2017
@fishcharlie
Copy link
Contributor

@bizz84 Is documentation going to be updated for this?

@bizz84
Copy link
Owner

bizz84 commented Aug 21, 2017

@fishcharlie You're right, I forgot! Will do tomorrow.

@bizz84
Copy link
Owner

bizz84 commented Aug 22, 2017

Done.

@fishcharlie
Copy link
Contributor

@bizz84 So is there a best practice for where to implement SwiftyStoreKit.shouldAddStorePaymentHandler? And how exactly does it handle it when someone goes to that link in Safari or tries to purchase from app store?

@zntfdr
Copy link
Author

zntfdr commented Aug 23, 2017

Hi @fishcharlie , please refer to the dedicated WWDC session to know everything about the new handler.

In short, the method is called whenever the user tries to buy an in-app purchase from the App Store (possible only from iOS 11).

If we return true to the method, then the purchase is handled exactly as if it was initialized inside the app (your storekit delegate methods will be called etc etc).

AFAIK the link in safari is used for testing purposes only (to imitate the in-app purchase from the App Store).

I hope this answer clears things up :)

@fishcharlie
Copy link
Contributor

@zntfdr And with this latest version of SwiftyStoreKit and that video will I be able to implement it all so users can subscribe to my auto-renewal subscription from the app store?

@zntfdr
Copy link
Author

zntfdr commented Aug 23, 2017

Yup 😄 .

The implementation requires very little effort from your side. Just update your pod and add something like

SwiftyStoreKit.shouldAddStorePaymentHandler = { (_ payment: SKPayment, _ product: SKProduct) in
  // check if user can purchase the product
  return true // or false if user shall not purchase the product yet
}

in your codebase.

Cheers!

@fishcharlie
Copy link
Contributor

@zntfdr Ok, I'm just going to have to figure out how to actually handle unlocking everything and the process after going in my app.

@zntfdr
Copy link
Author

zntfdr commented Aug 23, 2017

If you already process in-app purchases inside your app, there's nothing else that you must do:
adding the 4 lines in my previous reply is all I had to do on my project (already tested).

Best of luck 💪 💪

@fishcharlie
Copy link
Contributor

@zntfdr But it's all linked to a button. The SwiftyStoreKit.purchaseProduct is all in a button tap. So I'm not sure how that gets linked up.

@zntfdr
Copy link
Author

zntfdr commented Aug 23, 2017

Charlie, the purchaseProduct is automatically called for you. The new method shouldAddStorePaymentHandler is just asking you the authorizathion to do so. Please do a couple of tests and you'll see 👍

@fishcharlie
Copy link
Contributor

@zntfdr Is there a best practice for where to set SwiftyStoreKit.shouldAddStorePaymentHandler? Maybe in the didFinishLaunchingWithOptions? How did you handle that?

@zntfdr
Copy link
Author

zntfdr commented Aug 27, 2017

Hi Charlie!
Yes, this is something that you want to address/initialize ASAP in your app (a la completeTransactions).
Therefore, like you suggested, I'd implement it in the AppDelegate's didFinishLaunchingWithOptions method 😊

@fishcharlie
Copy link
Contributor

@zntfdr Awesome. Thanks so much. Trying it now. After looking into it more it seems easier than I thought it was. I'm honestly shocked at how easy it looks... kinda crazy

@zntfdr
Copy link
Author

zntfdr commented Aug 27, 2017

Haha, sure!
People at Apple are working hard to make things easy for us 😅 (and thanks to @bizz84 for the easy implementation in SwiftyStoreKit 💯 💯 )

@bizz84
Copy link
Owner

bizz84 commented Aug 27, 2017

Standing on the shoulders of giants... 🍏

@harryworld
Copy link

harryworld commented Sep 15, 2017

Thanks for the addition of the new feature. In fact, I saw this message in the transcript

Well, what if the user is in the middle of onboarding? Or if they're creating an account? Or if they've already unlocked the item they're trying to buy? Well, in this case, you can hold onto the payment and return false. Then, when the user is done onboarding or whatever they needed to do, simply add the payment to the payment queue as you would with a normal in-app purchase.

To my understanding, I check whether Onboarding is done, and return false if the user is going through it. Do I need to add another code piece to keep the SKPayment object here and submit later?

@zntfdr
Copy link
Author

zntfdr commented Sep 15, 2017

@harryworld:
If you return true, a new acquisition/payment will be triggered automatically, no further actions are necessary from your code/side.

@harryworld
Copy link

@zntfdr Thanks for quick reply, I know the implication for returning true. My question is about returning false, which is true in cases like Onboarding.

@zntfdr
Copy link
Author

zntfdr commented Sep 15, 2017

@harryworld:
I offer my apologies, I completely misunderstood! 😆

Yes, you can keep track of the user wish and trigger a new payment later (suggestion: show something to the user before launching a new purchase, maybe a prompt like "do you still want to purchase ..??").

Note: all this "keep track and launch new purchase later" has to be done by your code/app.

Cheers!

@harryworld
Copy link

So my question is what kind of API should I use to reuse that SKPayment?

@zntfdr
Copy link
Author

zntfdr commented Sep 15, 2017

Yes, as described in the documentation, you must reuse the same SKPayment.

@mahboud
Copy link

mahboud commented Sep 18, 2017

You must create a brand new new payment.

I'm not so sure about that. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/PromotingIn-AppPurchases/PromotingIn-AppPurchases.html states:

To defer a transaction:

  1. Save the payment to use when the app is ready. The payment already contains information about the product. Do not create a new SKPayment with the same product.

@zntfdr
Copy link
Author

zntfdr commented Sep 19, 2017

I stand corrected! Thanks (I've updated my previous comment)

@trevorturk
Copy link

trevorturk commented May 31, 2019

👋 apologies in advance for my newbie questions, but I'm having a hard time figuring out how to accomplish this. Please see this code sample with inline questions:

// unable to get this link to work in safari, it just takes me to google?
// itms-services://?action=purchaseIntent&bundleId=com.example.app&productIdentifier=product_name

// where I am so far in my implementation:
import StoreKit
import SwiftyStoreKit

class AppDelegate: SKPaymentTransactionObserver {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // I think this is correct and working properly, but who knows! :)
	SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
            for purchase in purchases {
                if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored {
                    if purchase.needsFinishTransaction {
                        SwiftyStoreKit.finishTransaction(purchase.transaction)
                    }
                    handlePurchase(purchase.productId)
                }
            }
        }
	    
	// if all IAPs would be supported (I only have one, anyway) you can just return true here?
	SwiftyStoreKit.shouldAddStorePaymentHandler = { payment, product in
            return true
	}
    }
  
    // ...and here? do I need both this and the above?
    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        return true
    }
  
    // do I need to worry about completing transactions here?
    // do I need to verify the receipt here? (I try via a shared function, but not sure if I have the receipt)
    // ... or can I see an auto renewing subscription status here, like the expiration date?
    // ...or can I just pass to my handlePurchase function if there's a local receipt?
    // is transaction.payment.productIdentifier == purchase.productId?
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            if transaction.transactionState == .purchased || transaction.transactionState == .restored {
                handlePurchase(transaction.payment.productIdentifier)
            }
        }
    }
}

// for normal IAPs done from within the app, and for completeTransactions, I do something like this:
func handlePurchase(_ productId: String) {
    let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
    SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
        if case .success(let receipt) = result {
            let purchaseResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productId, inReceipt: receipt)
            switch purchaseResult {
            case .purchased(let expiryDate, _):
                PurchaseHandler().setPaidExpiring(expiryDate)
            case .expired(let expiryDate, _):
                showError("Subscription expired \(expiryDate)", message: "Please renew your membership")
            case .notPurchased:
                showError("Receipt validation error. Please contact support", message: "Error code: notPurchased")
            }
        } else if case .error(let error) = result {
            showError("Receipt validation error. Please contact support", message: "\(error)")
        } else {
            showError("Receipt validation error. Please contact support", message: "No details available")
        }
    }
}

Apologies again for the series of questions, but I haven't been able to find any full examples and I'm terribly confused about this. Any help would be appreciated!

@Solublepeter
Copy link

Hi,

When using the existing implementation I received this rejection from Apple review...

"We noticed that your app did not fully meet the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.

When the user initiates an in-app purchase on the App Store, they are taken into your app to continue the transaction.

However, information about the subscription must be displayed to the user prior to the purchase:
• Title of publication or service
• Length of subscription (time period and content or services provided during each subscription period)
Price of subscription, and price per unit if appropriate

Your app must include links to your privacy policy and Terms of Use, and users should be able to access these links at any time.

Next Steps

To resolve this issue, please revise your app to include the missing information prior to initiating any auto-renewing subscription purchases within your app."

Anyone been through this part of the process? It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app, which seems a bit daft if jumping in from an App Store request?

I think that to implement this I would need SwiftyStoreKit to call a function in my code instead of completing purchase, but I may be wrong? Am I misinterpreting their message?

@zntfdr
Copy link
Author

zntfdr commented Jul 6, 2019 via email

@Solublepeter
Copy link

Solublepeter commented Jul 6, 2019

Thanks, that’s really helpful, can I ask how you approached it?

By which I mean- did you add your own handler for the system call, or modify your copy of SwiftStoreKit?

@zntfdr
Copy link
Author

zntfdr commented Jul 7, 2019

When I got this rejection, I didn't bothered doing it in the proper way.

This is my code:

SwiftyStoreKit.shouldAddStorePaymentHandler = { [weak self] _, _ in
  self?.showUpgrade()
  return false
}

In short, when the app receives a prompt to upgrade started from the App Store, I show the upgrade screen as if it was the user opening it from within the app.

Previously this was required as Apple wanted you to show all the subscription legalese stuff.
Now we don't need to show all of that any more, I'm assuming Apple wants your app to showcase the upgrade benefits from within the app before the user upgrades.

@NarayanammaGonuguntla
Copy link

NarayanammaGonuguntla commented Jan 30, 2020

Hi
It seems like they are asking me to show the same view controller I do before the user initiates a subscription in-app,l
This is correct. I also got rejected with the same message and this is what I had to do to get approved 😊
[…](#227

--Can u explain brief. same view controller means which view subscription page or payment page?

@bemisal01
Copy link

bemisal01 commented Aug 19, 2022

@zntfdr Hi my app first screen is already ProScreen so can i write this code in didFinishLaunchingWithOptions or should i still need to show ProScreen again in this method ?
SwiftyStoreKit.shouldAddStorePaymentHandler = { [weak self] _, _ in
return true
} //to do IAP directly from appstore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: purchase flows purchase processes, efficiency and failures help wanted iOS & iPadOS directly related to iPhone and iPad type: enhancement
Projects
None yet
Development

No branches or pull requests