'Why does App keeps crashing when I tap paid (IAP) because 'Index out of range' come with Memory and servers, too? (Swift)

I am not sure if someone in this StackOverflow has skills with Apple's Code to create IAP without an installed Cocoapod. (BTW: I am not fans of Cocoapod) I am here to show you what I find code that I has trouble with this situation for many hours.

First start here

import UIKit
import StoreKit

let nc = NotificationCenter.default

struct dataStoreMemory {
    static var fromLocal: [Int] = UserDefaults.standard.object(forKey: "Already Purchased") as? [Int] ?? [0]
}

var models = [SKProduct]()

var askedForEpisode: Int? = nil

enum Product: String, CaseIterable {
    case Episode2 = "com.CoreJezlleland.Episode2"
    case Episode3 = "com.CoreJezlleland.Episode3"
    case Episode4 = "com.CoreJezlleland.Episode4"
    case Episode5 = "com.CoreJezlleland.Episode5"
    case Episode6 = "com.CoreJezlleland.Episode6"
    case Episode7 = "com.CoreJezlleland.Episode7"
    case Episode8 = "com.CoreJezlleland.Episode8"
    case Episode9 = "com.CoreJezlleland.Episodes9"
    case Episode10 = "com.CoreJezlleland.Episode10"
}

extension EpisodesViewController {
    func fetchingProducts() {
        let request = SKProductsRequest(productIdentifiers: Set(Product.allCases.compactMap({ $0.rawValue })))
        request.delegate = self
        request.start()
    }
    

This is only one What caused crashed and console keep tell me 'Index out of range' and 'Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 3 beyond bounds for empty NSArray'

    func makePayment() {
        if SKPaymentQueue.canMakePayments() {
            print("before")
            let payment = SKPayment(product: models[(askedForEpisode ?? 0)])
            // MARK: Crashed
            // askedForEpisode has crashed because 'Index out of range'
            print("Crash")
            fetchingProducts()
            SKPaymentQueue.default().add(payment)
        } else {
            print("User unable to make payment")
        }
    }
    

(keep add code below)

    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        DispatchQueue.main.async {
            models = response.products
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        transactions.forEach({
            switch $0.transactionState {
            case .purchasing:
                print("Payment in processing")
            case .purchased:
                print("Payment Purchased")
                SKPaymentQueue.default().finishTransaction($0)
                thankYouPayment()
                
                dataStoreMemory.fromLocal.append(askedForEpisode!)
                UserDefaults.standard.set(dataStoreMemory.fromLocal, forKey: "Already Purchased")
            case .failed:
                print("Payment Fail")
                SKPaymentQueue.default().finishTransaction($0)
            case .restored:
                print("Payment Restored")
                self.dismiss(animated: true)
                thankYouPayment()
            case .deferred:
                print("Payment Deferred")
            @unknown default:
                print("Unknown Payment")
            }
        })
    }
    
    @objc func restoreTapped() {
        present(restoreWait, animated: true, completion: {
            SKPaymentQueue.default().restoreCompletedTransactions()
        })
    }
    
    func thankYouPayment() {
        restoreWait.dismiss(animated: true)
        let ac = UIAlertController(title: "Thank You", message: nil, preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "Alright!", style: .default))
        present(ac, animated: true)
        
        nc.post(name: NSNotification.Name("Reload List"), object: nil)
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        restoreWait.dismiss(animated: true)
        if queue.transactions.count == 0 {
            let ac = UIAlertController(title: "Restore Error", message: "You do not have any restore purchases in your account.", preferredStyle: .alert)
            ac.addAction(UIAlertAction(title: "OK", style: .default))
            present(ac, animated: true)
        } else {
            dataStoreMemory.fromLocal.removeAll()
            for x in queue.transactions {
                for num in 1...10 {
                    if x.transactionIdentifier == "com.CoreJezlleland.Episode\(num)" {
                        dataStoreMemory.fromLocal.append(num)
                    }
                }
            }
            thankYouPayment()
        }
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        restoreWait.dismiss(animated: true)
        let ac = UIAlertController(title: "Restore Error", message: "There was a problem while restoring your purchased. We recommend you try again.", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
        present(ac, animated: true)
    }
    
    func errorPayment() {
        restoreWait.dismiss(animated: true)
        let ac = UIAlertController(title: "Error Payment", message: "The payment was declined or something went wrong, so please try again.", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
        present(ac, animated: true)
    }
}

End of Code

Now I created this tableView.deselectRow in extension EpisodesViewController: UITableViewDataSource, UITableViewDelegate {}

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        
        switch detectBypassTime() {
        case .NoInternet:
            
            // (Alert popup) Do something if there is no internet
            
            print("Sorry, no internet.")
            
            let imageView = UIImageView()
            imageView.image = UIImage(named: "No Wifi")
            
                // no wifi : wifi.slash
                // wrong clock: Incorrect Clock
            imageView.translatesAutoresizingMaskIntoConstraints = false

            // Create the alert controller
            let alertController = UIAlertController(title: "\n\n\(noWifiTtitle)", message: noWifiMessage, preferredStyle: .alert)
            
            
            
            let cancelAction = UIAlertAction(title: cancelText, style: UIAlertAction.Style.cancel) {
                  UIAlertAction in
                   print("Cancel Pressed")
              }
            
         
            
              // Add the actions
              alertController.addAction(cancelAction)
              alertController.view.addSubview(imageView)
            
            imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 10).isActive = true
            imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor).isActive = true
            imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
            imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
            
              // Present the controller
            self.present(alertController, animated: true, completion: nil)
            
        case .BadTime:

            // (Alert popup) Do something is iPhone or iPad time / date is incorrect.
            
            print("Sorry, your device is incorrect time / date. Please fix it.")
            
            let imageView = UIImageView()
            imageView.image = UIImage(named: "Incorrect Clock")
            
                // no wifi : wifi.slash
                // wrong clock: Incorrect Clock
            imageView.translatesAutoresizingMaskIntoConstraints = false

            // Create the alert controller
            let alertController = UIAlertController(title: "\n\n\(clockBlockTtile)", message: clockBlockMessage, preferredStyle: .alert)
            
            
            
            let cancelAction = UIAlertAction(title: cancelText, style: UIAlertAction.Style.cancel) {
                  UIAlertAction in
                   print("Cancel Pressed")
              }
            
         
            
              // Add the actions
              alertController.addAction(cancelAction)
              alertController.view.addSubview(imageView)
            
            imageView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 10).isActive = true
            imageView.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor).isActive = true
            imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
            imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
            
              // Present the controller
            self.present(alertController, animated: true, completion: nil)
            
            
            
            
        case .Passed:
            // Move all didselect into here.
            print("Go ahead, enjoy the story!")
            
        }
        
        if dataStoreMemory.fromLocal.contains(indexPath.row) {
            accessStory(row: indexPath)
        } else {
            askedForEpisode = indexPath.row
            makePayment()
        }
    }

I forgot to mention that 'switch detectBypassTime() {}' shows up from above to create from these codes in First View Controller to check if WiFi or Data is on to work Apple Pay in 'EpisodeViewController'

extension WelcomeViewController {
    @objc func startTimeDetect() {
        let checkTimeBypass = detectBypassTime()
        switch checkTimeBypass {
        case .NoInternet:
            // Do something if there is no internet
            print("Sorry, no internet.")
        case .BadTime:
            // Do something is iPhone or iPad time / date is incorrect.
            print("Sorry, your device is incorrect time / date. Please fix it.")
        case .Passed:
            // Do something if time / date is normal.
            print("Go ahead, enjoy the story!")
        }
    }
}

enum BypassTimeMode {
    case NoInternet
    case BadTime
    case Passed
}

func detectBypassTime() -> BypassTimeMode {
    let dateformatter = DateFormatter()
    dateformatter.dateFormat = "yyyy-MM-dd hh:mm"
    if timeServer() != nil {
        
        let localTime = dateformatter.string(from: Date())
        let serverTime = dateformatter.string(from: timeServer()!) // Few mins later it will cause itself app crashed.
     
        if localTime == serverTime {
            return .Passed
        } else {
            return .BadTime
        }
    } else {
        return .NoInternet
    }
}

func timeServer() -> Date? {
    let url = URL(string: "http://worldtimeapi.org/api/ip")!
    do {
        var getTimeSince1970: Int!
        let jsonData = try Data(contentsOf: url, options: .alwaysMapped)
        let jsonDecoder = JSONDecoder()
        let jsonCatalogs = try? jsonDecoder.decode(JSONTime.self,from: jsonData)
        getTimeSince1970 = Int("\(jsonCatalogs!.unixtime)")
        let getDate = Date(timeIntervalSince1970: TimeInterval(getTimeSince1970!))
        return getDate
    } catch {
        return nil
        }}
    struct JSONTime: Decodable {
        let unixtime: Int
    }

Well, in that cases I used the codes for IAP and Memory that were created by Apple, Kinda hard to understand why it was hard to find a way to fix those issues with NSArray in making sure payment() stop crashing and pop alert for pay. Does someone know how to fix it?

I am trying to understand Apple's IAP codes that you may have recommended and not recommend to make solved with IAP issues. Thank you.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source