Currently working on submitting my application to Apple, and I'm experiencing a strange issue where my product identifiers are not loading immediately. Sometimes if you load the store quick enough, the prices are not loaded and you can't make a purchase. Apple is reporting that they can't make a purchase on their ipv6 network, but I've also seen this issue on ipv4.
Do you see any logic issues which would be causing this issue and/or ipv6 non-friendly code? Thank you.
Code Follows:
IAPManager:
import Foundation
import StoreKit
import RealmSwift
import AVFoundation
class IAPManager: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver
{
static let sharedInstance = IAPManager()
private let realm = try! Realm()
// SKRequest
var request:SKProductsRequest!
// Array of SKProducts
var products:[SKProduct] = []
// Audio
var audioPlayer: AVAudioPlayer!
// Received Response From Store
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
// Check for Response
self.products = response.products
}
// Creates String Array of Product Identifiers
func getStoreProductIdentifiers() -> [String]
{
var identifiers: [String] = []
let hintsPackXS = "net.identitywithheld.HintsPackXS"
let hintsPackS = "net.identitywithheld.HintsPackS"
let hintsPackM = "net.identitywithheld.HintsPackM"
let hintsPackL = "net.identitywithheld.HintsPackL"
let hintsPackXL = "net.identitywithheld.HintsPackXL"
let removeAds = "net.identitywithheld.RemoveAds"
identifiers.append(hintsPackXS)
identifiers.append(hintsPackS)
identifiers.append(hintsPackM)
identifiers.append(hintsPackL)
identifiers.append(hintsPackXL)
identifiers.append(removeAds)
return identifiers
}
// Perform Request with Identifiers
func performProductRequestForIdentifiers(identifiers:[String]){
// Create Set Out of String Array and Type Cast as Set<String>
// Note that sets are not in any paritcular order.
let products = NSSet(array: identifiers) as! Set<String>
// Set request to call based on products identifier
self.request = SKProductsRequest(productIdentifiers: products)
// Set Delegate to self (this class)
self.request.delegate = self
// Start Request
self.request.start()
}
// Request Store Products
// Gets Identifiers and Performs Requests (Ties Together)
func requestStoreProducts(){
self.performProductRequestForIdentifiers(identifiers: self.getStoreProductIdentifiers())
}
// Checks to see if user can make purchases
func setupPurchases() -> Bool
{
// Check to See if Can Make Payments
if SKPaymentQueue.canMakePayments(){
// Sets Self As Observer
SKPaymentQueue.default().add(self)
return true
}
else
{
return false
}
}
// Requests Payment Request
func createPaymentRequestForProduct(product:SKProduct)
{
let payment = SKMutablePayment(product: product)
payment.quantity = 1
// Starts Payment Process
SKPaymentQueue.default().add(payment)
}
// Payment Observer - Is Updated Whenever There's An Update
//MARK - Transaction Observer
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// For Each Transaction in Transaction Array [SKPaymentTransaction]
for transaction in transactions {
switch transaction.transactionState{
case .purchasing:
print("purchasing")
break
case .purchased:
print("purchased")
complete(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
case .deferred:
print("deferred")
break
case .failed:
print("failed")
fail(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
case .restored:
print("restored")
restore(transaction: transaction)
queue.finishTransaction(transaction) // Marks as Finished to Remove from Queue
break
}
}
}
// Complete Purchase
private func complete(transaction: SKPaymentTransaction) {
print("complete...")
deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
// Check Whether Sounds Are On
let gameDataObj = realm.objects(GameData.self).filter("id == 0").first
let soundOn = gameDataObj!.sound
if(soundOn == true)
{
let hintSound = NSURL(fileURLWithPath: Bundle.main.path(forResource: "purchaseComplete", ofType: "caf")!)
try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try? AVAudioSession.sharedInstance().setActive(true)
try? audioPlayer = AVAudioPlayer(contentsOf: hintSound as URL)
audioPlayer.volume = 0.5
audioPlayer!.prepareToPlay()
audioPlayer!.play()
}
// If On, Play Sound
}
// Restore Purchases
private func restore(transaction: SKPaymentTransaction) {
guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
print("restore... \(productIdentifier)")
deliverPurchaseNotificationFor(identifier: productIdentifier)
SKPaymentQueue.default().finishTransaction(transaction)
}
private func fail(transaction: SKPaymentTransaction) {
print("fail...")
if let transactionError = transaction.error as? NSError {
if transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction Error: \(transaction.error?.localizedDescription)")
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func deliverPurchaseNotificationFor(identifier: String?) {
guard let identifier = identifier else { return }
let gameDataObj = realm.objects(GameData.self).filter("id == 0")
var hints = gameDataObj.first!.hints
var hintsPurchase = false
if(identifier == "net.identitywithheld.HintsPackXS")
{
print("Extra Small Pack")
hints = hints + 10
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackS")
{
print("Small Pack")
hints = hints + 45
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackM")
{
print("Medium Pack")
hints = hints + 125
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackL")
{
print("Large Pack")
hints = hints + 200
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.HintsPackXL")
{
print("Extra Large Pack")
hints = hints + 500
hintsPurchase = true
}
else if(identifier == "net.identitywithheld.RemoveAds")
{
print("Remove Ads")
try! realm.write {
gameDataObj.first!.ads = false
}
}
// If Hint Purchase, Set Hints
if(hintsPurchase == true)
{
try! realm.write {
gameDataObj.first!.hints = hints
}
}
}
func getTextWidth(font: UIFont, text: String) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: 17, height: 17))
label.text = text
label.font = font
label.sizeToFit()
return label.frame.width
}
func getTextHeight(font: UIFont, text: String, width: CGFloat) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: 17))
label.text = text
label.font = font
label.numberOfLines = 0
label.sizeToFit()
return label.frame.height
}
}
App Delegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
openRealm()
// Override point for customization after application launch.
// Use Firebase library to configure APIs
FIRApp.configure()
GADMobileAds.configure(withApplicationID: adMobAppID)
// Store Call to Get Products if Can Make Purchases
if (IAPManager.sharedInstance.setupPurchases() == true)
{
IAPManager.sharedInstance.requestStoreProducts()
UserDefaults.standard.set(true, forKey: "IAPCapable")
UserDefaults.standard.synchronize()
}
else
{
UserDefaults.standard.set(false, forKey: "IAPCapable")
UserDefaults.standard.synchronize()
}
// Initialize the Chartboost library
Chartboost.start(withAppId: "identitywithheld", appSignature: "identitywithheld", delegate: nil)
return true
}
After testing this vigorously, I realized that I wasn't delaying the store load and so therefore the products were not fully loaded. This isn't an ipv6 issue.
To resolve this issue, I added an activity indicator and implemented a timer to check and see if the products were loaded before displaying the store.