iOS In-App Purchases: SKErrorDomain Code=0 returned when trying to redeem subscription offer within valid subscription period

Viewed 1458

Objective:

I would like to redeem a subscription offer while an autorenewable subscription is currently active and not expired.

What I've tried:

I set up a Store Kit demo project on Xcode 10.2 in order to test out the new subscription offers. After following the WWDC 2019 keynotes and much troubleshooting I was able to achieve the following:

  1. Successfully set up a 1-month autorenewable subscription using StoreKit.
  2. Hit the /verifyReceipt endpoint and successfully decrypted the receipt data.
  3. Set up local server and successfully generated signature.
  4. Set up subscription offer (1 month free) on Appstore Connect.
  5. Successfully redeemed subscription offer after renewable subscription has expired.

My current issue:

Whenever I try to claim the subscription offer and my autorenewable subscription is still active, I get the following error from StoreKit:

Error Domain=SKErrorDomain Code=0 "Cannot connect to iTunes Store" UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store}

Surprisingly, however, an alert box pops up upon trying to claim the offer saying "You're all set. Your purchase was successful. [Environment: Sandbox]" despite getting the above error message in the logs.

According to the Apple Documentation on SKErrorDomain (link: https://developer.apple.com/documentation/storekit/skerror/code), Code 0 is an "Unknown Error". This is the brick wall I've hit.

During the development process of this demo project, I received other error codes, such as ErrorCode=12, which means "invalid signature", which I resolved, so I'm sure there is nothing wrong with the signature. Check out my setup below for more context.

I urge you to disregard my non-optimal coding practices here. I hacked this together as a proof of concept, not for production.

This is the action for tapping the 'Claim Reward' button, which tries to redeem the subscription offer:

// PrimaryVC.swift

@IBAction func aClaimReward(_ sender: UIButton) {

        // Hard code offer information
        let username = "mail@mysandbox.com" // sandbox username
        guard let usernameData =  username.data(using: .utf8) else { return }
        let usernameHash = usernameData.md5().toHexString()

        // Enums for simplifying the product identifiers.
        let productIdentifier = IAPProduct.autoRenewable.rawValue
        let offerIdentifier = IAPProduct.reward.rawValue

        // Call prepare offer method and get discount in completion block
        IAPService.shared.prepareOffer(usernameHash: usernameHash, productIdentifier: productIdentifier, offerIdentifier: offerIdentifier) { (discount) in

            // Find the autorenewable subscription in products set
            guard let product = IAPService.shared.products.filter({ $0.productIdentifier == IAPProduct.autoRenewable.rawValue }).first else {
                return
            }

            // Complete transaction
            self.buyProduct(product: product, forApplicationUsername: usernameHash, withOffer: discount)
        }
    }

This is the code for the buyProduct() method, used at the end of the action above:

// PrimaryVC.swift

func buyProduct(product: SKProduct, forApplicationUsername usernameHash: String, withOffer offer: SKPaymentDiscount) {

        // Create payment object
        let payment = SKMutablePayment(product: product)

        // Apply username and offer to the payment
        payment.applicationUsername = usernameHash
        payment.paymentDiscount = offer

        // Add payment to paymentQueue
        IAPService.shared.paymentQueue.add(payment)
    }

I created an In-app purchases service class where much of the in-app purchase logic lives. This singleton is used in the button action detailed above, and the method used there follows:

// IAPService.swift

import SwiftyJSON
import Alamofire

// ...

func prepareOffer(usernameHash: String, productIdentifier: String, offerIdentifier: String, completion: @escaping (SKPaymentDiscount) -> Void) {

        // Create parameters dictionary
        let parameters: Parameters = [
            "appBundleID": "my.bundle.id",
            "productIdentifier": productIdentifier, // "my.product.id",
            "offerID": offerIdentifier, // "REFERRALBONUSMONTH"
            "applicationUsername": usernameHash
        ]

        // Generate new signature by making get request to local server.
        // I used the starter code from the wwdc2019 lecture on subscription offers

        AF.request("https://mylocalserver/offer", parameters: parameters).responseJSON { response in

            var signature: String?
            var keyID: String?
            var timestamp: NSNumber?
            var nonce: UUID?

            switch response.result {
            case let .success(value):
                let json = JSON(value)

                // Get required parameters for creating offer
                signature = json["signature"].stringValue
                keyID = json["keyID"].stringValue
                timestamp = json["timestamp"].numberValue
                nonce = UUID(uuidString: json["nonce"].stringValue)

            case let .failure(error):
                print(error)
                return
            }

            // Create offer
            let discountOffer = SKPaymentDiscount(identifier: offerIdentifier, keyIdentifier: keyID!, nonce: nonce!, signature: signature!, timestamp: timestamp!)

            // Pass offer in completion block
            completion(discountOffer)
        }
    }

Conclusion:

According to the WWDC2019 subscription offers lecture, users should be able to redeem a subscription offer even during an active subscription, but I continue to get SKErrorCode=0 when I try to redeem a subscription offer during an active subscription. I am able to redeem the subscription after the autorenewable subscription has expired, and I have verified the receipt and have seen the data for the subscription offer on the receipt.

Any ideas on where I might be going wrong? Or is this an issue on Apple's side?

0 Answers
Related