Validate receipts iOS - ios

Look like apple is making any type of update this month.... recently my app was rejected with this message
When validating receipts on your server, your server needs to be able
to handle a production-signed app getting its receipts from Appleā€™s
test environment. The recommended approach is for your production
server to always validate receipts against the production App Store
first. If validation fails with the error code "Sandbox receipt used
in production," you should validate against the test environment
instead.
My app was approved before ... this is the code that i m using
//Sandbox URL
//let url = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = bodyData
let task = URLSession.shared.dataTask(with: request) { (responseData, response, error) in
if let error = error {
completion(.failure(.other(error)))
} else if let responseData = responseData {
let json = try! JSONSerialization.jsonObject(with: responseData, options: []) as! Dictionary<String, Any>
//print(json)
let session = Session(receiptData: data, parsedReceipt: json)
self.sessions[session.id] = session
let result = (sessionId: session.id, currentSubscription: session.currentSubscription)
completion(.success(result))
}
}
task.resume()
}

You don't have to use a server. You can validate it on the client if you want. Or you could completely forgo any validation if you wanted (not recommended).
The rejection you are getting is most likely because this time around, they used a test env to validate IAP.
Their documentation states
If you are doing receipt validation, be sure to verify your receipt
with the production URL (https://buy.itunes.apple.com/verifyReceipt)
first. This applies even in the case where your app is used in the
sandbox environment. App Review will review the production version of
your app in the sandbox. When your app processes the receipt, it must
be capable of detecting the 21007 receipt status code and sending the
receipt to the sandbox receipt validation server
(https://sandbox.itunes.apple.com/verifyReceipt). Once your app is
approved and running in the production environment, sending the
receipt to the production server first is the correct action.
Notice that they don't specify where the receipt validation is done.
What your code lacks is the fallback to the sandbox. Hence why they rejected you this time around.

Related

iOS Swift URLSession POST request getting duplicated for slow API calls

I have a download task that work by first calling a REST API for which the server needs to generate a fairly large file which takes it several minutes to generate, as it is CPU and disk IO intensive. The client waits for the server to give a JSON response with the URL of the file it generated. The file download then starts after it gets the first result.
For the calls that generate a particularly large file, which causes the server to be very slow to respond, I am seeing duplicate requests that my code is not initiating.
Initially the someone who works on the server side told me about the duplicate requests. Then I set up a way to inspect network traffic. This was done by setting up a Mac connected to a wired network and enabling network sharing and using Proxyman to inspect the traffic from the iPhone to the API server. I see multiple instances of the same API request on the network layer but my code was never notified.
Code looks like this
#objc class OfflineMapDownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
#objc func download(){
let config = URLSessionConfiguration.background(withIdentifier: "OfflineMapDownloadSession")
config.timeoutIntervalForRequest = 500
config.shouldUseExtendedBackgroundIdleMode = true
config.sessionSendsLaunchEvents = true
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
getMapUrlsFromServer(bounds)
}
func getMapUrlsFromServer(){
var urlString = "http://www.fake.com/DoMakeMap.php"
if let url = URL(string: urlString) {
let request = NSMutableURLRequest(url: url)
//...Real code sets up a JSON body in to params...
request.httpBody = params.data(using: .utf8 )
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.timeoutInterval = 500
urlSession?.configuration.timeoutIntervalForRequest = 500
urlSession?.configuration.timeoutIntervalForResource = 500
request.httpShouldUsePipelining = true
let backgroundTask = urlSession?.downloadTask(with: request as URLRequest)
backgroundTask?.countOfBytesClientExpectsToSend = Int64(params.lengthOfBytes(using: .utf8))
backgroundTask?.countOfBytesClientExpectsToReceive = 1000
backgroundTask?.taskDescription = "Map Url Download"
backgroundTask?.resume()
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if (downloadTask.taskDescription == "CTM1 Url Download") {
do {
let data = try Data(contentsOf: location, options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
if let ctm1Url = jsonResult["CTM1Url"] as? String {
if let filesize = jsonResult["filesize"] as? Int {
currentDownload?.ctm1Url = URL(string: ctm1Url)
currentDownload?.ctm1FileSize = Int32(filesize)
if (Int32(filesize) == 0) {
postDownloadFailed()
} else {
startCtm1FileDownload(ctm1Url,filesize)
}
}
}
}
} catch {
postDownloadFailed()
}
}
}
There is more to this download class as it will download the actual file once the first api call is done. Since the problem happens before that code would be executed, I did not include it in the sample code.
The log from Proxyman shows that the API call went out at (minutes:seconds) 46:06, 47:13, 48:21, 49:30, 50:44, 52:06, 53:45
It looks like the request gets repeated with intervals that are just over 1 minute.
There is an API field where I can put any value and it will be echoed back to me by the server. I put a timestamp there generated with CACurrentMediaTime() and log in Proxyman shows that indeed its the same API call so there is no way my code is getting called multiple times. It seems as though the iOS networking layer is re-issuing the http request because the server is taking a very long time to respond. This ends up causing problems on the server and the API fails.
Any help would be greatly appreciated.
This sounds a lot like TCP retransmission. If the client sends a TCP segment, and the server does not acknowledge receipt within a short span of time, the client assumes the segment didn't make it to the destination, and it sends the segment again. This is a significantly lower-level mechanism than URLSession.
It's possible the HTTP server application this API is using (think Apache, IIS, LigHTTPd, nginx, etc.) is configured to acknowledge with the response data to save packeting and framing overhead. If so, and if the response data takes longer than the client's TCP retransmission timeout, you will get this behavior.
Do you have a packet capture of the connection? If not, try collecting one with tcpdump and reviewing it in Wireshark. If I'm right, you will see multiple requests, and they will all have the same sequence number.
As for how to fix it if that's the problem, I'm not sure. The server should acknowledge requests as soon as they are received.
I think the problem is in using URLSessionConfiguration.background(withIdentifier:) for this api call.
Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background. A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process. In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.
So the problem is that the system is retrying your request unnecessarily because of this wrong API usage.
Here's what I recommend -
Use default session configuration (NOT background).
Do this api call that initiates this long job, do NOT have client wait on this job, from server side return a job_id back to client as soon as this job is initiated.
Client can now poll server every X seconds using that job_id value to know about the status of the job, even can show progress on client side if needed.
When job is completed, and client polls next time, it gets the download URL for this big file.
Download the file (using default / background session configuration as you prefer).

Subscription Failure Error WSError(type: Starscream.ErrorType.upgradeError message: \"Invalid HTTP upgrade\", code: 400)?

I need to integrate subscription in my iOS app. The subscription works fine on localhost in graphiql. I have deployed my backend on Heroku. I am using apollo-server and not hasura. My subscriptions are not working for the url given by Heroku but it works fine on localhost. Queries and mutations work fine for both localhost and Heroku url. So I am trying to access my subscription from my iOS client. I have kept the base url as my local host. The queries and mutations part works for my iOS client but my subscription part is not working.
I have configured my Apollo client for subscription by adding this
let httpNetworkTransport = HTTPNetworkTransport(url: URL(string: "http://localhost:5000")!)
httpNetworkTransport.delegate = self
let webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: "http://localhost:5000")!))
let splitNetworkTransport = SplitNetworkTransport(
httpNetworkTransport: httpNetworkTransport,
webSocketNetworkTransport: webSocketTransport
)
return ApolloClient(networkTransport: splitNetworkTransport)
I also tried replacing http with ws as follows
let webSocketTransport = WebSocketTransport(request: URLRequest(url: URL(string: "ws://localhost:5000")!))
Subscription code is as follows
subscription = Network.shared.apollo.subscribe(subscription: GetHealthConsultationSubscriptionSubscription()){
[weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let result):
debugPrint(result.data?.healthConsultation.chiefComplaint)
case .failure(let error):
debugPrint(" Subscription Failure Error \(error)")
}
}
But I am getting error from my iOS client as follows
" Subscription Failure Error WSError(type: Starscream.ErrorType.upgradeError, message: \"Invalid HTTP upgrade\", code: 400)"
Also when I use Graphiql for my subscription and replace localhost with the Heroku url for my subscription , I get following error.
So there was no problem from my ios Code. The issue was with the free tier of Heroku I am using. I tried replacing my subscription with Hasura's Subscription https://hasura.io/learn/graphql/ios/subscriptions/1-subscription/ and it works.

Push Notification From Specific Device To Specific Device Using Firebase iOS Swift

I would be very thankful for help with Push Notifications. My app has a chat where users can send text messages directly to each other. But without Push Notifications it doesn't make much sense. It is all set up on Firebase. How could I send Push Notifications from a specific device to a specific device? I did try Firebase Notifications and OneSignal due to a recommendation on Stackoverflow. But I am only able to trigger the notifications from Firebase or OneSignal directly and not from a specific device to a specific device.
Does someone has experience with this?
If your data is being stored in Firebease I would use Cloud Messaging for Push notifications as well. There are multiple ways to achieve this messaging notification functions, I will resume the one I think is the easiest.
Assuming you are using FCM and you have configured your app for it.
First of all you need to store the token of the users device, this would be stored as a string with each of the users info:
let token = Messaging.messaging().fcmToken
So in your Firebase DB, in each of your users object data you would have a key storing a string with the token, if you want the user to receive notifications in multiple devices, you must store the multiple tokens of the user using an array of strings, or even an array of objects and store the device type, etc. Thats your choice and your needs.
The ideal push notification environment is usually composed by a middle server that receive the request and manage and send the notification to the corresponding users, in this case you can skip this and send the not directly from your app, using FCM POST request service:
Example of how to build a FCM POST request to send a notification:
let url = URL(string: "https://fcm.googleapis.com/fcm/send")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("key=--HERE-GOES-YOUR-API-KEY--", forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
Here you put all not data, read the docs to understand all the keys and variables that you can send, yo would need to make a previous firebase request in which you get the device tokens.
var notData: [String: Any] = [
"to" : "HERE YOU PUT THE DEVICE TOKEN(s)",
"notification": [
title : "not title",
body : "not body",
icon : "not icon"
],
"data": [
//More notification data.
]
]
And then you send the post request, It will return an object if the notification was succeed and more data.
request.httpBody = notData.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = String(data: data, encoding: .utf8)
print("responseString = \(responseString)")
}
task.resume()
I've been using this same solution for a mobile native app and a react web app and it works like a charm.
In Swift 4, everything that Karlo posted above works fine, except for the data(using:) function is not longer available.
That will need to be something like this:
request.httpBody = try? JSONSerialization.data(withJSONObject: notData, options: [])
Once I made that change everything compiled and worked beautifully.
You can target push notifications to a specific device easily. There's a tutorial given for that specific purpose here (https://firebase.google.com/docs/cloud-messaging/ios/first-message).
Now you can follow the below steps:
When the 'user' sends a message to 'other user', you can send the information of 'other user' to the server.
From server, you can send the push notification to the 'other user'

iTunes app rejected: missing feature to deliver subscription content

I'm implementing the In-App Purchase feature in my iOS app for the first time. I passed all test phases correctly and I submitted the app to review, but it was rejected for this reason:
Your app offers a content subscription but does not have a mechanism in place to support the requirement that the subscription content be available to the user on all of their iOS devices.
To fix the issue I have to:
include an optional user registration feature to deliver subscription content to all of the user's iOS devices.
My app don't require a user registration, so I'm wondering if it's really necessary.
Assuming the answer is "YES!", I don't know what really I have to do.
After the user registration, I need to save the receipt data on my host and then retrieve the data if the user change device?
Actually I get the receipt data in this way:
func validateReceipt() {
let receiptUrl = NSBundle.mainBundle().appStoreReceiptURL
let isSandox = receiptUrl?.absoluteString.containsString("sandboxReceipt")
if let receipt: NSData = NSData(contentsOfURL: receiptUrl!) {
let receiptdata: NSString = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
let transaction = GTLAppleTransactionBeanApiAppleTransactionBean()
transaction.receiptData = receiptdata as String
transaction.sandbox = isSandox
ReceiptService().validate(transaction, completion: validated)
} else {
receiptDetected = false
delegate.subscriptionUpdate()
}
}
The "GTLAppleTransactionBeanApiAppleTransactionBean()" is my GAE Bean and "ReceiptService()" call my GAE Endpoint service. This service check the receipt data with Apple.
A Swift example will be very much appreciated. :P
Sorry for my English.
Thanks, Alessio.

POST Queries in Swift for given website

I am trying to make queries to get the fuel type and consumption of a specified car (the user enters both make and model) for an iOS app written in Swift.
The app is targeted for Spain, and I have found a website that allows the user to enter make and model, and it returns the details for that car (http://coches.idae.es/portal/BaseDatos/MarcaModelo.aspx). I have seen using the tool WireShark, that the query is based on POST instead of GET. But I am not quite sure how I can make the requests within the app I am developing, or how to handle the info that is sent to me back from the sender.
Is there any way to make those requests to the given website? If so, I would really appreciate some help on the subject, I am new in iOS development and am looking forward to learning as much as possible.
Thanks :)
Many people prefer to use AFNetworking for making HTTP requests. However you don't need to do that. You said that its a POST request. Setting that up is easy even without AFNetworking using NSMutableURLRequest. I'm assuming you have a link to the API and not just to the aspx page. My Spanish is pretty weak so I can't look up the API reference for you but here's how you can make the request and receive data from the server. You will have to put the correct values and parse the responses:
let request = NSMutableURLRequest(URL: NSURL(string: "/* Paste URL here */")!)
request.HTTPMethod = "POST"
// Do this as many times are required for filling in the headers.
request.addValue("/* The value for the HTTP header */", forHTTPHeaderField: "/*The header field like Accept-Type, etc..*/")
// If you need an HTTP body too then make the JSONObj as a dictionary or array or whatever and then
let data = NSJSONSerialization.dataWithJSONObject(JSONObj, options: [])
request.HTTPBody = data // This needs to be NSData.
// Now make the request.
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, { (data, response, error) -> Void in
if error == nil
{
assert(data != nil)
let JSON = NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [NSObject: AnyObject]
// If you are using swift 2 this needs to be in a do try catch statement.
// TODO: Use JSON for whatever.
}
else
{
print(error!.localizedDescription)
}
}
task?.resume()
Let me know if you have any other questions or if the API doesn't use JSON or is completely different.

Resources