benjaminmayo / merchantkit

A modern In-App Purchases management framework for iOS.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

unresolving state

timprepscius opened this issue · comments

Again, as with the previous post, I'm not sure if this is actually an issue or not.

My app entered a state where the code:

    private func attemptFinishTaskFetchingLocalData(onFailure: () -> Void) {
        if let url = Bundle.main.appStoreReceiptURL, let isReachable = try? url.checkResourceIsReachable(), isReachable == true, let data = try? Data(contentsOf: url) {
            self.finish(with: .success(data))
        } else {
            onFailure()
        }
    }

Would call onFailure, I think, because maybe I installed a Release version over a Debug version.. Not sure exactly- url.checkResourceIsReachable() would do an NSCocoaDomainError.

But the receipt was in the KeyService.
But it was expired.

So, the end result was- I have an expired receipt, but isPurchased would be true. And it would never refresh it- removing from the keychain.

To solve it I changed the code in MerchantKit:

public func setup() {
    guard !self.hasSetup else { return }
    self.hasSetup = true
    
    self.storeInterface.setup(withDelegate: self)
    
    self.checkReceipt(updateProducts: .all, policy: .fetchElseRefresh, reason: .initialization)

To have ".fetchElseRefresh"....

But I really don't want to change your code at all..

What should have I done?

You can't call .fetchElseRefresh there because that means the user will be prompted to login at startup.

This sounds like a bug with StoreKit more than MerchantKit unfortunately. If the resource is unreachable, there's not much MerchantKit can do about it.

Ok, actually, I see I must getting the flow wrong.

So, if I delete the App, and reinstall. I can generate the state I described.

2019-05-27 16:03:49.705541-0400 BalletBox[9412:2570558] [Receipt] Created receipt fetcher for onlyFetch
2019-05-27 16:03:49.786299-0400 BalletBox[9412:2570558] [Receipt] Receipt fetch failed: receiptUnavailableWithoutRefresh
2019-05-27 16:03:49.787887-0400 BalletBox[9412:2570558] [Initialization] Merchant has been setup, with 1 registered product.

Hmmm, it seems like I should be triggering a refresh somehow, somewhere..

In production, you'll always have a receipt available so this state shouldn't happen. The App Store will packages the receipt with the app download. This doesn't happen in the sandbox environment.

Are receipts ever updated outside of purchase/restore?

I'm wondering if a renewing subscription will detect the renewal before detecting expiration.

Yeah, the StoreKit transaction observer fires upon renewal which updates the receipt.

Ok, I've spent several hours going through your code. And I think it's doing the right thing.. However...

When there are no receipts:

I need to use:

	func isSubscriptionValid () -> Bool
    {

		let state = merchant.state(for: ProductDatabase.monthlyAll)
		
#if DEBUG
		// this code is here to compensate during development
		// if the app is deleted and reinstalled, it will have no receipts, and it will stay perpetually
		// subscribed.  The observer also will not work, I have a feeling because the UUID changes.
		switch state
		{
			case .isPurchased(let info):
				let leeway : TimeInterval = 60 // use same leeway as MerchantKit
				Log.print("Purchase expiryData \(info.expiryDate) + \(leeway) >? \(Date())")
				return info.expiryDate?.addingTimeInterval(leeway) ?? Date.distantPast > Date()
				// I use ?? Date.distantPast, because on network error, the expiryDate will be nil...

			default:
				break
		}
		
		return false
#else
		return state.isPurchased
#endif
    }


This way the subscription dialog gets a queued. If you restore purchases the receipt is recreated. However, because of some edge condition, the product key is not signaled as changed, and the ProductInterfaceController doesn't tell the table to reload those keys. Or something. My brain has gone to mush.

It would be great if this edge condition could be handled by your library so that I would not need special debugging code.

Two ways, if there are no receipts, clear out the KeyChainService. I believe this will lead to the correct behavior.

Mush brain in, mush brain out. I apologize for the horrible grammar, spelling, etc.

Also, in any case I would suggest two small changes to the code, which would have clarified what was going on more quickly.

in
private func updateStorageWithValidatedReceipt(_ receipt: Receipt, updateProducts updateType: ReceiptUpdateType) -> Set<Product> {

change the logger messages to have the action taken, (because in most cases, there actually was nothing removed, but it seems like there was)

				self.logger.log(message: "Removed record for \(productIdentifier), given expiry date \(expiryDate) action \(result)", category: .purchaseStorage)

				self.logger.log(message: "Saved record: \(record)  action \(result)", category: .purchaseStorage)

You aren’t getting a change event because the state of the Merchant won’t have changed; you are wrapping the underlying value with your function, acting as if it is returning false when the state has been true the whole time.

For testing purposes, have you tried using .usefulForTestingAsPurchasedStateResetsOnApplicationLaunch configuration? This has ephemeral storage so you don’t need to worry about stale keychain state.

Updating the log message behaviour makes sense to me.

Addressed log message in 6ed1a01.