When restoring I get a crash fatal error: unexpectedly found nil while unwrapping an Optional value
Whenever I click the restore button to check if I had bought a package before, to test it out in sandbox. The function processTransactionRestore gets called to verify the receipt but the code is being executed multiple times around 20 every time. I have a print saying "Processing Transaction Restore" and in the logs you see the print 22 times and since I have a uialert to tell you that it failed. I get the uialert 22 times non stop.
Any help?
Subscription.swift
// When restore product
func processTransactionRestore(transaction: SKPaymentTransaction) {
if transaction.payment.productIdentifier == "com.example.year" {
print("Bought Year Package: Premium")
}
print("Processing Transaction Restore")
// Validate Receipt
Validation.validateReceipt( completed: {
if Validation.isSubscribed() {
DispatchQueue.main.async{
// Go To Protected Page: MainController
let mainPage = self.storyboard?.instantiateViewController(withIdentifier: "MainController") as! MainController
let mainPageNav = UINavigationController(rootViewController: mainPage)
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.rootViewController = mainPageNav
}
} else {
print("Not Validated! Nothing to restore.. Trying Again..")
// Alert
let alert = UIAlertController(title: "Nothing to Restore", message: "Unable to find any products to restore.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default, handler: nil))
alert.addAction(UIAlertAction(title: "Try Again", style: UIAlertActionStyle.default, handler: { _ in
// Repeating Validate Receipt
Validation.validateReceipt( completed: {
if Validation.isSubscribed() {
DispatchQueue.main.async{
// Go To Protected Page: MainController
let mainPage = self.storyboard?.instantiateViewController(withIdentifier: "MainController") as! MainController
let mainPageNav = UINavigationController(rootViewController: mainPage)
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.rootViewController = mainPageNav
}
} else {
print("Not Validated! Nothing to process.. Trying Again..")
// Alert
let alert = UIAlertController(title: "Nothing to Process", message: "Unable to process any products.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default, handler: nil))
alert.addAction(UIAlertAction(title: "Try Again", style: UIAlertActionStyle.default, handler: { _ in
print("Try Again button")
}))
self.present(alert, animated: true, completion: nil)
}
})
}))
self.present(alert, animated: true, completion: nil)
}
})
}
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("Transactions Restored")
for transaction in queue.transactions {
processTransactionRestore(transaction: transaction)
}
}
Restore button
#IBAction func restoreButton(_ sender: Any) {
print("Restoring Purchase")
SKPaymentQueue.default().restoreCompletedTransactions()
}
Logs
Restoring Purchase
Transactions Restored
Bought Year Package: Premium
Processing Transaction Restore
Validating Receipt...
Processing Transaction Restore
Validating Receipt...
Processing Transaction Restore
Validating Receipt...
Bought Year Package: Premium
Processing Transaction Restore
Validating Receipt...
Processing Transaction Restore
Validating Receipt...
Bought Year Package: Premium
Processing Transaction Restore
Validating Receipt...
Bought Year Package: Premium
Processing Transaction Restore
Validating Receipt...
Processing Transaction Restore
Validating Receipt...
Bought Year Package: Premium
Not Validated! Nothing to restore.. Trying Again..
fatal error: unexpectedly found nil while unwrapping an Optional value
Validation.swift
public class Validation: UIViewController {
class func isSubscribed() -> Bool {
// Setting subscribed to be whatever its saved as
if let mainSubscribed: Bool = customKeychainWrapperInstance.bool(forKey: "isSubscribed") {
print("Found Key! for isSubscribed")
subscribed = mainSubscribed
} else {
print("No Key Found: Validating Receipt!")
subscribed = false
}
return subscribed
}
class func validateReceipt(completed:(() -> Void)?) {
var date_today_ms: Int64 = 0
var date_expires_ms: Int64 = 0
var statusCode: Int = 1
let receiptUrl = Bundle.main.appStoreReceiptURL
do {
print("Validating Receipt...")
// Run Activity Indicator
//
// Getting Data
let receipt: Data = try Data(contentsOf:receiptUrl!)
let receiptdata: NSString = receipt.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) as NSString
let request = NSMutableURLRequest(url: NSURL(string: "https://example.com/verify.php")! as URL)
let session = URLSession.shared
request.httpMethod = "POST"
request.httpBody = receiptdata.data(using: String.Encoding.ascii.rawValue)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if(error != nil) {
print(error!.localizedDescription)
let jsonStr = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("Error could not parse JSON: '\(String(describing: jsonStr))'")
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
} else {
if let parseJSON = json {
if (parseJSON["status"] as? Int == 0) {
print("Sucessfully returned purchased receipt data!")
statusCode = 0
// Checking "latest_receipt_info"
if let receiptInfo: NSArray = parseJSON["latest_receipt_info"] as? NSArray {
let lastReceipt = receiptInfo.lastObject as! NSDictionary
// Get last receipt
print("\nLast Receipt Information: \n", lastReceipt)
// Getting Expired Time in MS
if let expiresString = lastReceipt["expires_date_ms"] as? String, let expiresMS = Int64(expiresString) {
date_expires_ms = expiresMS / 1000
}
// Remember doing anything in the JSON causes failure
}
} else if (parseJSON["status"] as? Int == 21002) {
print("Status Code: 21002 - The data in the receipt-data property was malformed or missing.")
// try it again maybe 2 times then cancel it
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
} else { // add more status codes statements
print("Status Code: Something went wrong!")
// cancel it
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
}
} else {
let jsonStr = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("Receipt Error: \(String(describing: jsonStr))")
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
}
}
} catch {
print("Error: (Receipt to JSON)")
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
}
// ........ Outside the `do try JSON {` ....................
// is Validated [0]
if statusCode == 0 {
// [1] Checking 'Expired Date' //
print("date_expires: \(date_expires_ms)") // Date Expired
// Getting Todays Time in MS
date_today_ms = Int64(Date().timeIntervalSince1970) //* 1000
print("date_today: \(date_today_ms)")
if date_expires_ms < date_today_ms {
print("The product is EXPIRED!")
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
} else {
print("The product is ACTIVE!")
// isSubscribed = True
subscribed = true
saveSubscriptionKey()
}
// [2] Checking Blank ...
}
if let completedBlock = completed {
completedBlock();
}
})
task.resume()
} catch {
print("Error: (Receipt URL)")
// isSubscribed is False
subscribed = false
saveSubscriptionKey()
if let completedBlock = completed {
completedBlock();
}
}
}
}
Related
I'm working on my app (iOS) to get AdMob and AdSense earnings information. But I've been having trouble getting specifics from them. I've already created the credentials and client ID from my Google account, but I'm not sure where to put them.
I tried carefully following many methods from this link but was never successful.
My first step: During startup, check to see if you are logged in or out.
import FirebaseAuth
import GoogleSignIn
var currentPID = ""
func checkGoogleAccountStatus() {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if error != nil || user == nil {
print("Signed out")
self.loginButton()
} else {
print("Signed in")
let userAccess: String = user!.authentication.accessToken
self.googledSignedInSuccess(googleToken: userAccess, tokenID: user!.authentication.idToken!)
let dateformatter = DateFormatter()
dateformatter.dateFormat = "MMMM d, yyyy h:mm:ss a"
let expiredToken = user?.authentication.accessTokenExpirationDate
print("Token Expired: \(dateformatter.string(from: expiredToken!))")
}
}
}
Successful
My second step: When I tapped the button to log in, an alert controller appeared to see if the log in was successful. It will display the alert controller's profile picture, name, and email address.
#objc func loginTapped() {
let adMobScope = "https://www.googleapis.com/auth/admob.report"
let adSenseScope = "https://www.googleapis.com/auth/adsensehost"
let additionalScopes = [adMobScope,adSenseScope]
let signInConfig = GIDConfiguration.init(clientID: "<My URL Schemes>")
GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self, hint: nil, additionalScopes: additionalScopes) { user, error in
guard error == nil else { return }
guard let user = user else { return }
if let profiledata = user.profile {
let grantedScopes = user.grantedScopes
if grantedScopes == nil || !grantedScopes!.contains(adMobScope) {
print("AdMob not Granted...")
} else {
print("AdMob Granted!")
}
if grantedScopes == nil || !grantedScopes!.contains(adSenseScope) {
print("AdSense not Granted...")
} else {
print("AdSense Granted!")
}
//let userId: String = user.userID ?? ""
let givenName: String = profiledata.givenName ?? ""
let familyName: String = profiledata.familyName ?? ""
let email: String = profiledata.email
let userToken: String = user.authentication.idToken!
let userAccess: String = user.authentication.accessToken
let credential = GoogleAuthProvider.credential(withIDToken: userToken, accessToken: userAccess)
Auth.auth().signIn(with: credential) { result, error in
if let error = error {
print(error.localizedDescription)
let alert = UIAlertController(title: "Error", message: "Something went wrong, please try again.", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .cancel, handler: {_ in return })
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
if let imgurl = user.profile?.imageURL(withDimension: 300) {
let absoluteurl: String = imgurl.absoluteString
let alert = UIAlertController(title: "\(givenName) \(familyName)", message: "\n\n\n\n\n\n\n\(email)\nLogin Successful", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .cancel, handler: {_ in
// MARK: Do something to update
self.checkGoogleAccountStatus()
})
let imgViewTitle = UIImageView()
imgViewTitle.translatesAutoresizingMaskIntoConstraints = false
imgViewTitle.layer.borderColor = UIColor(named: "Font Color")?.cgColor
imgViewTitle.layer.borderWidth = 3
imgViewTitle.layer.cornerRadius = 50
imgViewTitle.clipsToBounds = true
alert.view.addSubview(imgViewTitle)
imgViewTitle.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor, constant: -28).isActive = true
imgViewTitle.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor).isActive = true
imgViewTitle.widthAnchor.constraint(equalToConstant: 100).isActive = true
imgViewTitle.heightAnchor.constraint(equalToConstant: 100).isActive = true
DispatchQueue.global().async {
if let data = try? Data(contentsOf: URL(string: absoluteurl)! ) { if let image = UIImage(data: data) { DispatchQueue.main.async { imgViewTitle.image = image } } }
}
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "\(givenName) \(familyName)", message: "\(email)\nLogin Successful", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .cancel, handler: {_ in
// MARK: Do something to update
self.checkGoogleAccountStatus()
})
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
}
}
}
}
When I logged in, it asked for permission to access AdMob and AdSense, followed by a pop-up alert that said I had successfully logged in.
My third step: Getting the PID from Google AdMob / AdSense
import CurlDSL
import Gzip
func googledSignedInSuccess(googleToken: String, tokenID: String) {
guard let url = URL(string: "https://admob.googleapis.com/v1/accounts/") else { return }
do {
try CURL(#"curl -H "Authorization: Bearer \#(googleToken)" "\#(url)""#).run { data, response, error in
if let error = error { print("Error took place \(error)"); return }
if let response = response as? HTTPURLResponse {
if response.statusCode != 200 {
print("Error: \(response)")
} else {
if let data = data {
do {
if let rawJSON = try? JSONDecoder().decode(GetAdMobInfo.self, from: data) {
currentPID = rawJSON.account[0].publisherID
print("Successful: \(currentPID)")
self.adMob_gettingReport(pid: currentPID, token: googleToken)
}
}
}
}
}
}
} catch { print("Failed.") }
}
struct GetAdMobInfo: Codable {
let account: [Account]
}
struct Account: Codable {
let name, publisherID, reportingTimeZone, currencyCode: String
enum CodingKeys: String, CodingKey {
case name
case publisherID = "publisherId"
case reportingTimeZone, currencyCode
}
}
It was success, I was able to get my PID and writed to currentPID as string.
My final step, which failed:
func adMob_gettingReport(pid: String, token: String) {
guard let url = URL(string: "https://admob.googleapis.com/v1/accounts/\(pid)/mediationReport:generate") else { return }
let reportData = "--data #- << EOF {\"report_spec\": {\"date_range\": {\"start_date\": {\"year\": 2020, \"month\": 4, \"day\": 1}, \"end_date\": {\"year\": 2020, \"month\": 4, \"day\": 1} },\"dimensions\": [\"AD_SOURCE\", \"AD_UNIT\", \"PLATFORM\"], \"metrics\": [\"ESTIMATED_EARNINGS\"]}} EOF"
do {
try CURL(#"curl -X POST "\#(url)" -H "Authorization: Bearer \#(token)" -H "Content-Type: application/json" \#(reportData)"#).run { data, response, error in
if let error = error { print("Error took place \(error)"); return }
if let response = response as? HTTPURLResponse {
if response.statusCode != 200 {
print("Error: \(response)")
} else {
if let data = data {
print("Getting AdMob Successful")
let decompressedData: Data
if data.isGzipped { decompressedData = try! data.gunzipped() }
else { decompressedData = data }
var getLineFromString: [String] = []
getLineFromString += String(data: decompressedData, encoding: .utf8)!.components(separatedBy: "\n")
for checkLine in getLineFromString {
print("Line: \(checkLine)")
}
}
}
}
}
} catch { print("Failed.") }
}
Attempting to obtain earnings information from AdMob and AdSense, but it kept saying failing in print. This is where I've been for nearly two months. What did I overlook?
So my goal is to have congruent functionality both on the iOS simulator in Xcode and as well as a physical device on TestFlight. So currently, I have a function that handles refunds in my app. On the simulator the function runs perfectly fine in the order I expect it to, but the print statements execute in the wrong order which I'm assuming is the reason for misbehaviour on TestFlight simulations.
Here is the method:
#IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName else { return }
guard let user = Auth.auth().currentUser else { return }
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest()
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}
Basically what I have going on is a simple refund request being made to Stripe and a second after I have an asyncAfter code block with some database cleanup in it. I have to do the asyncAfter or else the refund request gets beat out by the other async tasks by speed.
So I took my knowledge of DispatchGroups and decided to implement it since I have an async task in a for loop that I need to be completed before every other task. So I expected this to work fine, despite the order of the print statements being incorrect, but when I ran the exact block of code on my phone via TestFlight, I made a refund and the cell was still showing up in the tableview, meaning the document wasn't deleted from the database properly.
I've been having some terrifying experience recently with DispatchGroups and TestFlight and I just honestly hope to fix all this and have these problems come to an end temporarily. Any suggestions on how I can fix this method to prevent incorrect order on TestFlight?
UPDATE Decided to use a completion handler instead to do the same functionality:
func makeRefundRequest(refundMade: #escaping ((Bool) -> ())) {
let backendURLForRefund = "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund"
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid else { return }
let url = URL(string: backendURLForRefund)!
let json: [String: Any] = [
"payment_intent": id
]
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: json)
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
guard let taskError = error?.localizedDescription else { return }
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
self?.showAlert(title: "Refund Request Error", message: "There was an error making the refund request. \(taskError)")
refundMade(false)
return
}
}
task.resume()
refundMade(true)
}
}
And then I just slapped this method in the actual refund process method itself:
#IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName else { return }
guard let user = Auth.auth().currentUser else { return }
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest { (response) in
if response == false {
return
} else {
let group = DispatchGroup()
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (querySnapshot, error) in
guard error == nil else {
print("The guests couldn't be fetched.")
return
}
guard querySnapshot?.isEmpty == false else {
print("The user did not bring any guests.")
return
}
for guest in querySnapshot!.documents {
let name = guest.documentID
group.enter()
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
guard error == nil else {
print("The guests couldn't be deleted.")
return
}
print("Guests deleted with purchase refund.")
group.leave()
}
}
}
group.notify(queue: .main) {
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
guard error == nil else {
print("Error trying to delete the purchased event.")
return
}
print("The purchased event was succesfully removed from the database!")
}
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}
This actually does not work fine, yes the refund goes through on Stripe and the database is cleaned up for 3 minutes, but the print statements print in incorrect order and also the document magically reappears in the Firestore database 3 minutes after physically seeing it be deleted, how can I prevent this and make sure they print in correct order and execute in correct order to work properly on TestFlight? Is this an issue in my DispatchGroup implementation? Or is it something completely different?
I think it's important that no matter what you end up doing you should learn how to do everything anyway. I would advise against this approach below but it's what you originally started so let's finish it regardless, so you know how dispatch grouping works. Once you've gotten a handle on this, refine it by replacing the dispatch group with a Firestore transaction or batch operation. The point of the transaction or batch operation is so all of the documents are deleted atomically, meaning they all go or none go. This simplifies things greatly and they are very basic! And the documentation for them is very clear.
The final thing I would suggest is perhaps integrating some recursion, meaning that if something fails it can retry automatically. Recursive functions are also very basic so just learn how to write one in Playground first and then apply it here. Just take it one step at a time and you'll get it down within a day or two. But this is the first step so carefully read what I wrote and understand why I did what I did.
func makeRefundRequest(refundMade: #escaping (_ done: Bool) -> Void) {
getStripePaymentIntentID { (paymentid) in
guard let id = paymentid,
let url = URL(string: "https://us-central1-xxxxxx-41f12.cloudfunctions.net/createRefund") else {
refundMade(false) // always call completion when you exit this function
return
}
let json: [String: Any] = ["payment_intent": id]
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONSerialization.data(withJSONObject: json)
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
if let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let _ = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
refundMade(true) // completion: true
} else {
if let error = error {
print(error)
}
refundMade(false) // completion: false
}
}
task.resume()
// do not call the completion of this function here because you just made a network call
// so call the completion of this function in that call's completion handler
}
}
#IBAction func cancelPurchasePressed(_ sender: UIButton) {
guard let nameOfEvent = selectedEventName,
let user = Auth.auth().currentUser else {
return
}
let alertForCancel = UIAlertController(title: "Cancel Purchase", message: "Are you sure you want to cancel your purchase of a ticket to \(nameOfEvent)? You will receive full reimbursement of what you paid within 5 - 10 days.", preferredStyle: .alert)
let cancelPurchase = UIAlertAction(title: "Cancel Purchase", style: .default) { (purchaseCancel) in
// put the UI in a loading state
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
self.refundLoading.alpha = 1
self.refundLoading.startAnimating()
self.makeRefundRequest { (done) in
if done {
self.db.collection("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests").getDocuments { (snapshot, error) in
guard let snapshot = snapshot,
!snapshot.isEmpty else {
if let error = error {
print(error)
}
return
}
let group = DispatchGroup() // instatiate the dispatch group outside the loop
for doc in snapshot.documents {
group.enter() // enter on each loop iteration
let name = doc.documentID
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)/guests/\(name)").delete { (error) in
if let error = error {
print(error)
}
group.leave() // leave no matter what the outcome, error or not
// what do you do when this document didn't delete?
// by doing all your deleting in a transaction or batch
// you can ensure that they all delete or none delete
}
}
group.notify(queue: .main) { // done with loop, make final network call
self.db.document("student_users/\(user.uid)/events_bought/\(nameOfEvent)").delete { (error) in
if let error = error {
print(error)
}
// put the UI back to normal state
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
} else { // refund was not made, put the UI back into normal state
self.refundLoading.stopAnimating()
self.refundLoading.alpha = 0
self.ticketFormButton.isHidden = false
self.cancelPurchaseButton.isHidden = true
self.viewPurchaseButton.isHidden = true
}
}
}
alertForCancel.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alertForCancel.addAction(cancelPurchase)
present(alertForCancel, animated: true, completion: nil)
}
I am using Apple Pay with Stripe and it works fine when there is no shipping is available.
When there is a shipping address selected it always gives invalid address error. (It works with Stripe Sandbox Key)
I am using STKPaymentContext API and follow steps from the below link.
https://stripe.com/docs/mobile/ios/basic
in the configuration, I have written this.
let config = STPPaymentConfiguration.shared()
config.requiredShippingAddressFields = [.postalAddress, .phoneNumber,.name]
Not sure what is wrong here.
Here is how it looks.
HERE IS MY CODE
extension CheckoutTableViewController:STPPaymentContextDelegate{
func paymentContext(_ paymentContext: STPPaymentContext, didUpdateShippingAddress address: STPAddress, completion: #escaping STPShippingMethodsCompletionBlock) {
guard
let buyerPostalCode = address.postalCode,
let buyerCountry = address.country,
let productId = self.productDetailsData?.productId
else{
completion(.invalid, nil, nil, nil)
return
}
guard let phone = address.phone, phone.count > 0 else {
completion(.invalid,RatesError.phoneNumberRequired,[],nil)
return
}
var shipmentItem:[String:Any] = [:]
shipmentItem["order_amount"] = self.productCost
shipmentItem["actual_weight"] = 8
shipmentItem["height"] = 7
shipmentItem["width"] = 10
shipmentItem["length"] = 13
shipmentItem["currency"] = "USD"
shipmentItem["destination_postal_code"] = buyerPostalCode
shipmentItem["destination_country_code"] = buyerCountry
shipmentItem["product_id"] = productId
shipmentItem["category"] = "fashion"
enum RatesError:Error,LocalizedError{
case NoDeliveryOptionsFound
case phoneNumberRequired
public var errorDescription: String? {
switch self {
case .NoDeliveryOptionsFound:
return "No couriers are available at the address.\nPlease try with different address."
case .phoneNumberRequired:
return "Please enter phone number."
}
}
}
fetchShippingOptions(forItem: shipmentItem, completionSuccess: {[weak self] (response) in
guard let self = `self` else {
return
}
if
let responseValue = response as? [String:Any],
let rates = responseValue["rates"] as? [[String:Any]]{
self.shippingRates = []
for rate in rates{
if let fullName = rate["courier_display_name"] as? String,
let identifier = rate["courier_id"] as? String,
let amount = rate["shipment_charge_total"] as? Double,
let detail = rate["full_description"] as? String
{
let method = PKShippingMethod()
method.amount = NSDecimalNumber.init(value: amount.currency)
method.identifier = identifier
method.label = fullName
method.detail = detail.replacingOccurrences(of: fullName, with: "")
self.shippingRates.append(method)
}
}
completion(.valid, nil, self.shippingRates, self.shippingRates.first)
}else{
completion(.invalid,RatesError.NoDeliveryOptionsFound,[],nil)
}
}) { (error) in
completion(.invalid,error,[],nil)
}
}
func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
if let paymentOption = paymentContext.selectedPaymentOption {
self.lblPaymentMethod.text = paymentOption.label
} else {
self.lblPaymentMethod.text = "Select Payment"
}
if let shippingMethod = paymentContext.selectedShippingMethod {
if let selectedRate = self.shippingRates.first(where: { (method) -> Bool in
guard let leftValue = method.identifier, let rightValue = shippingMethod.identifier else{
return false
}
return leftValue == rightValue
}){
self.lblAddress.text = selectedRate.label
self.shippingCharges = Double(truncating: selectedRate.amount).currency
self.lblShippingCharges.text = "$\(shippingCharges)"
self.getStripeFees(forAmount: self.productCost + self.shippingCharges)
}
} else {
self.lblAddress.text = "Select Address"
}
self.updateTotalCost()
}
func paymentContext(_ paymentContext: STPPaymentContext, didFailToLoadWithError error: Error) {
let alertController = UIAlertController(
title: "Error",
message: error.localizedDescription,
preferredStyle: .alert
)
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: { action in
// Need to assign to _ because optional binding loses #discardableResult value
// https://bugs.swift.org/browse/SR-1681
_ = self.navigationController?.popViewController(animated: true)
})
let retry = UIAlertAction(title: "Retry", style: .default, handler: { action in
self.paymentContext?.retryLoading()
})
alertController.addAction(cancel)
alertController.addAction(retry)
self.present(alertController, animated: true, completion: nil)
}
func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: #escaping STPPaymentStatusBlock) {
self.callPaymentIntentAPI(paymentContext, didCreatePaymentResult: paymentResult, completion: completion)
}
func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) {
OperationQueue.main.addOperation {
SVProgressHUD.dismiss()
}
let title: String
let message: String
switch status {
case .error:
title = "Error"
message = error?.localizedDescription ?? ""
UIAlertController.showAlert(withTitle: title, andMessage: message, andButtonTitles: ["Okay"]) {[weak self] (selectedIndex) in
OperationQueue.main.addOperation {
self?.navigationController?.popViewController(animated: true)
}
}
case .success:
title = "Success"
message = "Your purchase was successful!"
UIAlertController.showAlert(withTitle: title, andMessage: message, andButtonTitles: ["Okay"]) {[weak self] (selectedIndex) in
OperationQueue.main.addOperation {
self?.onPaymentCompletion?()
var isControllerFound:Bool = false
for controller in self?.navigationController?.viewControllers ?? []{
if (controller is ProductDetailsViewController) || (controller is ChatVC){
isControllerFound = true
self?.navigationController?.popToViewController(controller, animated: true)
break
}
}
if !isControllerFound{
self?.navigationController?.popViewController(animated: true)
}
}
}
case .userCancellation:
return()
#unknown default:
return()
}
}
}
Finally, I found an error.
Apple calls didUpdateShippingAddress method at the time of payment but it doesn't provide all information for security purposes. So in my case phone number validation was causing that error.
So I removed the below code from that method.
guard let phone = address.phone, phone.count > 0 else {
completion(.invalid,RatesError.phoneNumberRequired,[],nil)
return
}
I'm working on this subscription system for my app,it took a few days.But as I'm working more on it,it makes more problems.
Sometimes it happens that the app crashes when clicking on buy subscription.
Also I'm having the problem of not knowing when to stop the Please wait.... alert.I need to place the code in:
case .Purchasing:
self.alert_show()
break
But I don't know where to end it,I need to know the information when is the alert from iTunes loaded than to stop the Please wait.... alert.
The biggest problem right now that I'm facing is the crashing sometimes when i click on the Buy Button.
The crashing happens when first time launching the app,and clicking one the Buy Subscription.
Here is the code for the Buy Subscription:
#IBAction func buy_sub(sender: AnyObject) {
let payment:SKPayment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
Here is the error that I'm getting.
fatal error: unexpectedly found nil while unwrapping an Optional value..
I would like to share with you my source code of the subscription.If you have any advices what to add,or to correct or to correct this problems that i have it would be great.
Here is the full source code of the Subscription View:
class SubscriptionViewController: UIViewController ,SKPaymentTransactionObserver, SKProductsRequestDelegate {
//var productID = ""
var product: SKProduct!
#IBOutlet var buy_trial_button: UIButton!
override func viewDidAppear(animated: Bool) {
if(SKPaymentQueue.canMakePayments()) {
} else {
buy_trial_button.enabled = false
message_alert("Please enable IAPS to continue(Credit card information required in your iTunes account).")
}
//validateReceipt()
let keystore = NSUbiquitousKeyValueStore.defaultStore()
if keystore.objectForKey("expiration_date") != nil{
let expiration: AnyObject? = keystore.objectForKey("expiration_date")
let today = NSDate()
let expiredate = expiration as! NSDate
print("expiredate is %#",expiredate,today)
// var date1 = NSDate()
// var date2 = NSDate()
if(today.compare(expiredate) == NSComparisonResult.OrderedDescending){
print("today is later than expiredate")
validateReceipt()
print("Validating")
}else if(today.compare(expiredate) == NSComparisonResult.OrderedAscending){
print("today is earlier than expiredate")
self.performSegueWithIdentifier("subscriptionPassed", sender: self)
}
}else{
print("First time launch")
}
}
override func viewDidLoad() {
super.viewDidLoad()
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
self.getProductInfo()
//SKPaymentQueue.defaultQueue().addTransactionObserver(self)
//self.getProductInfo()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillDisappear(animated: Bool) {
}
#IBAction func private_policy(sender: AnyObject) {
let openLink = NSURL(string : "http://arsutech.com/private_policy.php")
UIApplication.sharedApplication().openURL(openLink!)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "subscriptionPassed")
{
}
}
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse){
let products = response.products
if products.count != 0
{
product = products[0] as SKProduct
print(product.localizedTitle + "\n" + product.localizedDescription)
}
}
func getProductInfo(){
if (SKPaymentQueue.canMakePayments()){
let productID:NSSet = NSSet(object: "com.sxxxxxxxxxxxxxxxxx")
let request:SKProductsRequest = SKProductsRequest(productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
}
}
#IBAction func buy_sub(sender: AnyObject) {
let payment:SKPayment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
#IBAction func buy_pro(sender: AnyObject) {
let openLink = NSURL(string : "https://itunes.apple.com/us/app/home-workouts-exercises-mma/id1060747118?mt=8")
UIApplication.sharedApplication().openURL(openLink!)
}
#IBAction func exit(sender: AnyObject) {
exit(0)
}
func validateReceipt(){
alert_show()
let mainBundle = NSBundle.mainBundle() as NSBundle;
let receiptUrl = mainBundle.appStoreReceiptURL;
let isPresent = receiptUrl?.checkResourceIsReachableAndReturnError(NSErrorPointer());
if(isPresent == true){
let data = NSData(contentsOfURL: receiptUrl! );
// Create the JSON object that describes the request
let requestContents = NSMutableDictionary();
//let encodeddata = data!.base64EncodedStringWithOptions(NSDataBase64EncodingOptions());
let encodeddata = data!.base64EncodedString();
//print("encodeddata = \(encodeddata)");
requestContents.setObject(encodeddata, forKey: "receipt-data");
requestContents.setObject("c40f23af1aa44e159aezzzzzzzzzzzzzz", forKey: "password");
var requestData : NSData?
do{
requestData = try NSJSONSerialization.dataWithJSONObject(requestContents, options: NSJSONWritingOptions());
}catch{
// NSLog("Error in json data creation at verifyPaymentReceipt");
}
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString
let file = "\(documentsPath)/requestData"
if(NSFileManager.defaultManager().createFileAtPath(file, contents: data, attributes: nil)){
NSLog("File %# ",file);
}
else{
//NSLog("error File %# ",file);
}
if(requestData != nil){
//let strRequestData = NSString(data: requestData!, encoding: NSUTF8StringEncoding);
//print(" strRequestData = \(strRequestData)");
// Create a POST request with the receipt data.
//https://buy.itunes.apple.com/verifyReceipt
//https://sandbox.itunes.apple.com/verifyReceipt
let storeURL = NSURL(string: "https://sandbox.itunes.apple.com/verifyReceipt");
let storeRequest = NSMutableURLRequest(URL: storeURL!);
storeRequest.HTTPMethod = "POST";
storeRequest.HTTPBody = requestData;
// Make a connection to the iTunes Store on a background queue.
let queue = NSOperationQueue();
NSURLConnection.sendAsynchronousRequest(storeRequest, queue: queue, completionHandler: { (response : NSURLResponse?, data : NSData?, error : NSError?) -> Void in
if(error != nil){
//Handle Error
}
else{
let d = NSString(data: data!, encoding: NSUTF8StringEncoding);
// NSLog("DATA:%#", d!);
let dataA = d!.dataUsingEncoding(NSUTF8StringEncoding)
var jsonResponseInternal: NSMutableDictionary?
do{
jsonResponseInternal = try NSJSONSerialization.JSONObjectWithData(dataA!,options: NSJSONReadingOptions.AllowFragments) as? NSMutableDictionary;
//print(jsonResponseInternal);
}catch{
// NSLog("Parsing issue : verifyPaymentReceipt");
}
var jsonResponse: NSMutableDictionary?
do{
jsonResponse = try NSJSONSerialization.JSONObjectWithData(data!,
options: NSJSONReadingOptions.AllowFragments) as? NSMutableDictionary;
//print(jsonResponse);
}catch{
// NSLog("Parsing issue : verifyPaymentReceipt");
}
if(jsonResponse != nil){
if(jsonResponse != nil){
//NSLog("Expiration Date: %#", jsonResponse!);
//print("Passed")
/*
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "Purchase")
NSUserDefaults.standardUserDefaults().synchronize()
*/
//self.performSegueWithIdentifier("subscriptionPassed", sender: self)
if jsonResponse!["status"] as? Int == 0 {
//print("Sucessfully returned internal receipt data")
if let receiptInfo: NSArray = jsonResponse!["latest_receipt_info"] as? NSArray {
let lastReceipt = receiptInfo.lastObject as! NSDictionary
var trial_period: Bool = false
// Get last receipt
//print("LAST RECEIPT INFORMATION \n",lastReceipt)
print("Last from internal memory",lastReceipt["original_transaction_id"])
//var is_trial = lastReceipt["is_trial_period"] as! Bool
//var date_bought =
//var date_expires =
// Format date
//print(is_trial)
// Format date
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
// Get Expiry date as NSDate
let subscriptionExpirationDate: NSDate = formatter.dateFromString(lastReceipt["expires_date"] as! String) as NSDate!
print("\n - DATE SUBSCRIPTION EXPIRES = \(subscriptionExpirationDate)")
let currentDateTime = NSDate()
print(currentDateTime)
if var is_trial:Bool = false{
if(lastReceipt["is_trial_period"] as? String == "Optional(\"true\")"){
is_trial = true
trial_period = is_trial
}else if(lastReceipt["is_trial_period"] as? String == "Optional(\"false\")"){
is_trial = false
trial_period = is_trial
}
}
if (subscriptionExpirationDate.compare(currentDateTime) == NSComparisonResult.OrderedDescending) {
self.alrt_close()
print("Pass");
print("The trial period is \(trial_period)")
let keystore = NSUbiquitousKeyValueStore.defaultStore()
keystore.setObject(subscriptionExpirationDate,forKey:"expiration_date")
keystore.synchronize()
self.performSegueWithIdentifier("subscriptionPassed", sender: self)
} else if (subscriptionExpirationDate.compare(currentDateTime) == NSComparisonResult.OrderedAscending) {
print("Not Pass");
print("Subscription expired")
self.alrt_close()
//self.message_alert("Subscription expired")
}
}
}
}
}
}
});
}
}
}
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Purchasing:
//self.alrt_close()
break
case .Deferred:
self.alert_show()
break
case .Purchased:
alrt_close()
self.validateReceipt()
//self.setExpirationDate()
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
//dismissViewControllerAnimated(false, completion: nil)
break
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as! SKPaymentTransaction)
print("Not called Expired")
message_alert("Error while purchasing!")
break
case .Restored:
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
print("Restored")
break
default:
break
}
}
}
}
//Alrt
func message_alert(let message_A:String){
let alert = UIAlertController(title: "", message: "\(message_A)", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
func alert_show(){
let alert = UIAlertController(title: nil, message: "Please wait...", preferredStyle: .Alert)
alert.view.tintColor = UIColor.blackColor()
let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(frame: CGRectMake(10, 5, 50, 50)) as UIActivityIndicatorView
loadingIndicator.hidesWhenStopped = true
loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
loadingIndicator.startAnimating();
alert.view.addSubview(loadingIndicator)
presentViewController(alert, animated: true, completion: nil)
}
func alrt_close(){
self.dismissViewControllerAnimated(false, completion: nil)
}
}
I am trying to return a result from a JSON object but unable to do so. I am new to Swift so kindly explain me how to do so. In the below code I want to return json_level_number in the return of function fetchNumberOfSections () where i have hard coded as 5 right now return 5. If i declare a variable json_level_number just above the reachability code it sort of solves the problem but then it is returning '0' for the first time. The API returns 2 each time.
Code as below:
func fetchNumberOfSections () -> Int {
if Reachability.isConnectedToNetwork() == true {
// Below code to fetch number of sections
var urlAsString = "http://themostplayed.com/rest/fetch_challenge_sections.php"
urlAsString = urlAsString+"?apiKey="+apiKey
print (urlAsString)
let url = NSURL(string: urlAsString)!
let urlSession = NSURLSession.sharedSession()
let jsonQuery = urlSession.dataTaskWithURL(url, completionHandler: { data, response, error -> Void in
if (error != nil) {
print(error!.localizedDescription)
}
do {
let jsonResult = (try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)) as! NSDictionary
let json_level_number: String! = jsonResult["level_number"] as! String
//
dispatch_async(dispatch_get_main_queue(), {
// self.dateLabel.text = jsonDate
// self.timeLabel.text = jsonTime
print(json_level_number)
// self.activityIndicatorStop()
})
}
catch let errorJSON {
print (errorJSON)
// alert box code below
let alert = UIAlertController(title: "JSON Error!", message:"Error processing JSON.", preferredStyle: .Alert)
let action = UIAlertAction(title: "OK", style: .Default) { _ in
// Put here any code that you would like to execute when
self.dismissViewControllerAnimated(true, completion: {})
}
alert.addAction(action)
self.presentViewController(alert, animated: true, completion: nil)
// alert box code end
}
})
jsonQuery.resume()
// End
}
else {
print("Internet connection FAILED")
self.performSegueWithIdentifier("nointernet", sender: nil)
}
return 5
}