bizz84 / SwiftyStoreKit

Lightweight In App Purchases Swift framework for iOS 8.0+, tvOS 9.0+ and macOS 10.10+ ⛺
MIT License
6.55k stars 796 forks source link

purchaseProduct Fail #305

Open WillMays1 opened 6 years ago

WillMays1 commented 6 years ago

Platform

In app purchase type

Environment

Not working in production but working in sandbox

When purchasing a subscription, it verifies the receipt correctly. Once it asks the user to buy the subscription I am getting a "Purchase Fail" error. Below is my code:

func verifySubscription(){
        let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "**********")
        SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
            switch result {
            case .success(let receipt):
                let purchaseResult = SwiftyStoreKit.verifySubscription(
                    type: .autoRenewable,
                    productId: "**********",
                    inReceipt: receipt)
                switch purchaseResult {
                case .purchased(let expiryDate, let receiptItems):
                    print("******************** Expiration Date: \(expiryDate) ********************")
                    print("******************** Receipt: \(receiptItems) ********************")
                case .expired(let expiryDate, let receiptItems):
                    print("******************** Expiration Date: \(expiryDate) ********************")
                    print("******************** Receipt: \(receiptItems) ********************")
                    self.purchaseSubscription()
                case .notPurchased:
                    print("******************** User never purchased ********************")
                    self.purchaseSubscription()
                }

            case .error(let error):
                print("******************** Receipt Verification Error: \(error) ********************")
            }
        }
    }

func purchaseSubscription(){
        SwiftyStoreKit.purchaseProduct("**********", atomically: true) { result in
            if case .success(let purchase) = result {
                if purchase.needsFinishTransaction {
                    SwiftyStoreKit.finishTransaction(purchase.transaction)
                }
            } else {
**I AM GETTING THIS ERROR AND ITS NOT LETTING USER BUY SUBSCRIPTION**
                print("******************** Purchase Error ********************")
                let alertController = UIAlertController(title: "Error", message: "Cannot purchase content.\nPlease try again later", preferredStyle: .alert)
                let yesAction = UIAlertAction(title: "Ok", style: .default) { (action) -> Void in }
                alertController.addAction(yesAction)
                self.present(alertController, animated: true, completion: nil)
            }
        }
    }
WillMays1 commented 6 years ago

Can anyone help me? I need this resolved asap

haemi commented 6 years ago

@WillMays1 did you find a solution for this? I'm stumbling upon this as well

WillMays1 commented 6 years ago

@haemi No I have not found a solution. I have contacted Apple and they said it is an issue with SwiftyStoreKit and to contact the developer. I have tried to get ahold of him but have had no luck

bizz84 commented 6 years ago

@WillMays1 Apologies for the late reply.

What error do you get when you make the purchase?

You can use this code to find out:

SwiftyStoreKit.purchaseProduct("your-product-id", quantity: 1, atomically: true) { result in
    switch result {
    case .success(let purchase):
        print("Purchase Success: \(purchase.productId)")
    case .error(let error):
        switch error.code {
        case .unknown: print("Unknown error. Please contact support")
        case .clientInvalid: print("Not allowed to make the payment")
        case .paymentCancelled: break
        case .paymentInvalid: print("The purchase identifier was invalid")
        case .paymentNotAllowed: print("The device is not allowed to make the payment")
        case .storeProductNotAvailable: print("The product is not available in the current storefront")
        case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
        case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
        case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
        }
    }
}

(if you need to test in production you can replace the print statements with analytics calls).

Also make sure to double check all your iTunes Connect settings, and read all relevant information.

WillMays1 commented 6 years ago

Do I need to add the quantity: 1 when purchasing an auto renewable subscription?

bizz84 commented 6 years ago

@WillMays1 no, you don't. Quantity is an optional parameter, it defaults to 1 if you omit it.

0a1c commented 6 years ago

I have this issue as well -- it works when I use a sandbox ID, but not when I try it with a real apple ID. I get the 'unknown error.' Is there any way to fix this?

0a1c commented 6 years ago

Any ideas? My purchase is not working when I use a real Apple ID, and I suspect that is the reason why my build got rejected. Is this because of Swifty?

BoilingLime commented 6 years ago

@bizz84 I have the same issue. And the error return in result is .unkown. But the users are billed by apple/ It doesn't occur on every purchaseProduct but only sometimes.

phr85 commented 6 years ago

@bizz84 Any news about this issue?

bizz84 commented 6 years ago

Apologies for the late reply everyone.

SwiftyStoreKit returns "Unknown error" when a transaction is .failed, but there is no transaction error. According to the docs, this shouldn't happen though:

open class SKPaymentTransaction : NSObject {
    // Only set if state is SKPaymentTransactionFailed
    @available(iOS 3.0, *)
    open var error: Error? { get }
}

SwiftyStoreKit only returns an error if a failed transaction shows up in the payment queue. So I'm inclined to think this is a problem in StoreKit or in your IAP setup in iTunes connect.

Also:

Are you calling completeTransactions() on app startup?

If you don't you could get mixed up failed transactions that were pending before you started the new purchase (this typically results in the completion block called immediately after calling purchaseProduct()).

Mathengel commented 6 years ago

I am experiencing the same issue. It works most of the time, but occasionally we get a user who is gets both a payment confirmation alert. img_5772 and then immediately gets the unknown purchase error alert. img_5773

They insist that they are subscribed, are able to view the subscription in their phone settings, and have been billed.

Calling completeTransactions() on app start up.

What might be the issue in IAP setup in Itunes Connect? Especially if this is only an occasional problem?

Mathengel commented 6 years ago

I don't understand how both alerts could be shown. Where does that first success alert come from? It must be generated before verifying the receipt, because that happens inside the result from SwiftyStoreKit.purchaseProduct returned as .success.

So how am I getting both? How is it both successfully making the purchase, while also getting an error, before even verifying the receipt from the purchase?

The second is generated only if the result from SwiftyStoreKit.purchaseProduct returns as .error

SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in NetworkActivityIndicatorManager.networkOperationFinished()

                switch result {
                case .success(let product):
                    // fetch content from your server, then:
                    if product.needsFinishTransaction {
                        SwiftyStoreKit.finishTransaction(product.transaction)
                    }
                    self.verifyPurchaseOfSubscription(purchase, completion: { result in
                        purchaseSuccessHandler(result)
                    }, verifyErrorHandler: { error in
                        verifyErrorHandler(error)
                    })
                    print("Purchase Success: \(product.productId)")
                case .error(let error):
                    purchaseErrorHandler(error)
                }
            }

func alertForPurchaseError(error: SKError) -> UIAlertController {
    switch error.code {

    case .unknown:
        Mixpanel.mainInstance().track(event: "Unknown Purchase Error", properties: ["userID" : self.userID])
        return alertWithTitle(title: "Purchase failed", message: "Unknown error. Please contact support or try again in a moment.")
dsb92 commented 6 years ago

Also getting this error. Maybe it's a new bug introduced from Apple because the error delegate from StoreKit fires along with a success delegate like described in this post.. Just weird it work in test but not in production.

RaimundWege commented 6 years ago

First: I've nothing to do with this project. Second: I've found this issue on google and I've noticed that the last post here is only 11 hours ago and right now I'm also facing an issue with the missing error object of a failed transaction object in a production environment...

So I guess its a new bug from Apple.

rafaelapaula commented 6 years ago

@RaimundWege did you discovered if it is a bug from apple?

AdieOlami commented 5 years ago

Hi please if you got it working kindly assist with my issue https://github.com/bizz84/SwiftyStoreKit/issues/412

RaimundWege commented 5 years ago

To solve that issue in my own store implementation I had to add a check for nil. I thought that the transaction in the case of SKPaymentTransactionStateFailed always provides an error object. But in some cases the error can be nil and in my implementation I've added this nilvalue to a dictionary... the solution was to add NSNull in that case: error ? error : [NSNull null].

I guess this happens when there are some old zombie transactions which were never finished correctly.

shi-rudo commented 5 years ago

i am have the same error at the moment. the problem occurred suddenly.

@RaimundWege could you give as a more detailed explanation how you handled nil case?

RuslanMaleyNikanor commented 5 years ago

It seems there are some problems at the Apple side. This issue happens all over the world

RaimundWege commented 5 years ago

@shi-rudo I only check the error result directly after receiving the notification and when the error object is nil I replace it with [NSNull null]:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    [[ATLogger shared] logDebug:[NSString stringWithFormat:@"Running transactions: %lu", (unsigned long)queue.transactions.count]];
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:
                [self purchaseTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing:
                break;
            case SKPaymentTransactionStateDeferred:
                break;
            default:
                break;
        }
    }
}

- (void)failTransaction:(SKPaymentTransaction *)transaction {
    [[ATLogger shared] logError:[NSString stringWithFormat:@"ATStoreKitController fail transaction: %@", transaction.transactionIdentifier] withError:transaction.error];
    [self postPurchaseFailureNotificationForIdentifier:transaction.payment.productIdentifier andError:transaction.error];
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)postPurchaseFailureNotificationForIdentifier:(NSString *)identifier andError:(NSError *)error {
    NSDictionary *userInfo = @{kUserInfoProduct:[self productForIdentifier:identifier],
                               kUserInfoError:(error ? error : [NSNull null])}; // <--- CHECK FOR NIL HERE
    [[NSNotificationCenter defaultCenter] postNotificationName:ATPurchaseFailureNotification object:identifier userInfo:userInfo];
}

I recommend to use logs for purchase process debugging.