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

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 on my logs:

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?

Replies

Hi orlando-maven, it seems you're experiencing the same issue as we do.


I believe this error happens because user with an active subscription isn't charged, when they redeem your offer. The discount will still be applied at the moment of renewal (from my experience in stage environment).


I agree that this error is ambigious and Apple should at least provide some specific code for such cases.

Hi there!


We are also experiencing the same issue where we cannot apply a promotional offer (i.e. 1 free month) on an active subscription in the sandbox environment. We are, however, able to successfully subscribe with no promo offers and also subscribe to a new subscription with a promo offer for one free month.


Is there any update on this? It's extremely challenging to test this in the sandbox environment without knowing if this will be the case in production.


Thanks!

Eann