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:
- Successfully set up a 1-month autorenewable subscription using StoreKit.
- Hit the /verifyReceipt endpoint and successfully decrypted the receipt data.
- Set up local server and successfully generated signature.
- Set up subscription offer (1 month free) on Appstore Connect.
- 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?