I have created a class which handles the purchases on In-App Purchases and also the validating of receipts. A while ago I used to use the transactionReceipt property on an SKPaymentTransaction, but have updated my code a fair amount and now use appStoreReceiptURL on the [NSBundle mainBundle].
Basically it seems as though my receipt is being sent to Apple's server in an acceptable manner, but I keep getting the status code of 21002. In auto-renewable subscriptions I know that this means the receipt is not in an acceptable format, however I have no idea what this status means in regard to an in-app purchase receipt.
Here is the local method validating the receipt:
/**
* Validates the receipt.
*
* #param transaction The transaction triggering the validation of the receipt.
*/
- (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction
{
// get the product for the transaction
IAPProduct *product = self.internalProducts[transaction.payment.productIdentifier];
// get the receipt as a base64 encoded string
NSData *receiptData = [[NSData alloc] initWithContentsOfURL:[NSBundle mainBundle].appStoreReceiptURL];
NSString *receipt = [receiptData base64EncodedStringWithOptions:kNilOptions];
NSLog(#"Receipt: %#", receipt);
// determine the url for the receipt verification server
NSURL *verificationURL = [[NSURL alloc] initWithString:IAPHelperServerBaseURL];
verificationURL = [verificationURL URLByAppendingPathComponent:IAPHelperServerReceiptVerificationComponent];
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:verificationURL];
urlRequest.HTTPMethod = #"POST";
NSDictionary *httpBody = #{#"receipt" : receipt,
#"sandbox" : #(1)};
urlRequest.HTTPBody = [NSKeyedArchiver archivedDataWithRootObject:httpBody];
[NSURLConnection sendAsynchronousRequest:urlRequest
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
// create a block to be called whenever a filue is hit
void (^failureBlock)(NSString *failureMessage) = ^void(NSString *failureMessage)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
// log the failure message
NSLog(#"%#", failureMessage);
// if we have aready tried refreshing the receipt then we close the transaction to avoid loops
if (self.transactionToValidate)
product.purchaseInProgress = NO,
[[SKPaymentQueue defaultQueue] finishTransaction:transaction],
[self notifyStatus:#"Validation failed." forProduct:product],
self.transactionToValidate = nil;
// if we haven't tried yet, we'll refresh the receipt and then attempt a second validation
else
self.transactionToValidate = transaction,
[self refreshReceipt];
}];
};
// check for an error whilst contacting the server
if (connectionError)
{
failureBlock([[NSString alloc] initWithFormat:#"Failure connecting to server: %#", connectionError]);
return;
}
// cast the response appropriately
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
// parse the JSON
NSError *jsonError;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
// if the data did not parse correctly we fail out
if (!json)
{
NSString *responseString = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
NSString *failureMessage = [[NSString alloc] initWithFormat:#"Failure parsing JSON: %#\nServer Response: %# (%#)",
data, responseString, #(httpResponse.statusCode)];
failureBlock(failureMessage);
return;
}
// if the JSON was successfully parsed pull out status code to check for verification success
NSInteger statusCode = [json[#"status"] integerValue];
NSString *errorDescription = json[#"error"];
// if the verification did not succeed we fail out
if (statusCode != 0)
{
NSString *failureMessage = [[NSString alloc] initWithFormat:#"Failure verifying receipt: %#", errorDescription];
failureBlock(failureMessage);
}
// otherwise we have succeded, yay
else
NSLog(#"Successfully verified receipt."),
[self provideContentForCompletedTransaction:transaction productIdentifier:transaction.payment.productIdentifier];
}];
}
The important PHP function on the server does this:
/**
* Validates a given receipt and returns the result.
*
* #param receipt Base64-encoded receipt.
* #param sandbox Boolean indicating whether to use sandbox servers or production servers.
*
* #return Whether the reciept is valid or not.
*/
function validateReceipt($receipt, $sandbox)
{
// determine url for store based on if this is production or development
if ($sandbox)
$store = 'https://sandbox.itunes.apple.com/verifyReceipt';
else
$store = 'https://buy.itunes.apple.com/verifyReceipt';
// set up json-encoded dictionary with receipt data for apple receipt validator
$postData = json_encode(array('receipt-data' => $receipt));
// use curl library to perform web request
$curlHandle = curl_init($store);
// we want results returned as string, the request to be a post, and the json data to be in the post fields
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData);
$encodedResponse = curl_exec($curlHandle);
curl_close($curlHandle);
// if we received no response we return the error
if (!$encodedResponse)
return result(ERROR_VERIFICATION_NO_RESPONSE, 'Payment could not be verified - no response data. This was sandbox? ' . ($sandbox ? 'YES' : 'NO'));
// decode json response and get the data
$response = json_decode($encodedResponse);
$status = $response->{'status'};
$decodedReceipt = $response->{'receipt'};
// if status code is not 0 there was an error validation receipt
if ($status)
return result(ERROR_VERIFICATION_FAILED, 'Payment could not be verified (status = ' . $status . ').');
// log the returned receipt from validator
logToFile(print_r($decodedReceipt, true));
// pull out product id, transaction id and original transaction id from infro trurned by apple
$productID = $decodedReceipt->{'product_id'};
$transactionID = $decodedReceipt->{'transaction_id'};
$originalTransactionID = $decodedReceipt->{'original_transaction_id'};
// make sure product id has expected prefix or we bail
if (!beginsWith($productID, PRODUCT_ID_PREFIX))
return result(ERROR_INVALID_PRODUCT_ID, 'Invalid Product Identifier');
// get any existing record of this transaction id from our database
$db = Database::get();
$statement = $db->prepare('SELECT * FROM transactions WHERE transaction_id = ?');
$statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
$statement->execute();
// if we have handled this transaction before return a failure
if ($statement->rowCount())
{
logToFile("Already processed $transactionID.");
return result(ERROR_TRANSACTION_ALREADY_PROCESSED, 'Already processed this transaction.');
}
// otherwise we insert this new transaction into the database
else
{
logToFile("Adding $transactionID.");
$statement = $db->prepare('INSERT INTO transactions(transaction_id, product_id, original_transaction_id) VALUES (?, ?, ?)');
$statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
$statement->bindParam(2, $productID, PDO::PARAM_STR, 32);
$statement->bindParam(3, $originalTransactionID, PDO::PARAM_STR, 32);
$statement->execute();
}
return result(SUCCESS);
}
The actual PHP script being executed is:
$receipt = $_POST['receipt'];
$sandbox = $_POST['sandbox'];
$returnValue = validateReceipt($receipt, $sandbox);
header('content-type: application/json; charset=utf-8');
echo json_encode($returnValue);
Comparing your PHP with mine (which I know works) is difficult because I am using HTTPRequest rather than the raw curl APIs. However, it seems to me that you are setting the "{receipt-data:..}" JSON string as merely a field in the POST data rather than as the raw POST data itself, which is what my code is doing.
curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData); // Possible problem
$encodedResponse = curl_exec($curlHandle);
Compared to:
$postData = '{"receipt-data" : "'.$receipt.'"}'; // yay one-off JSON serialization!
$request = new HTTPRequest('https://sandbox.itunes.apple.com/verifyReceipt', HTTP_METH_POST);
$request->setBody($postData); // Relevant difference...
$request->send();
$encodedResponse = $request->getResponseBody();
I have changed my variable names a bit to make them match up with your example.
the code 21002 means "The data in the receipt-data property was malformed or missing."
you can find it in
https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
below code is my class for appstore in-app verifyRecepip, GuzzleHttp is required, you can install it by composer require guzzlehttp/guzzle https://github.com/guzzle/guzzle
<?php
namespace App\Libraries;
class AppStoreIAP
{
const SANDBOX_URL = 'https://sandbox.itunes.apple.com/verifyReceipt';
const PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt';
protected $receipt = null;
protected $receiptData = null;
protected $endpoint = 'production';
public function __construct($receipt, $endpoint = self::PRODUCTION_URL)
{
$this->receipt = json_encode(['receipt-data' => $receipt]);
$this->endpoint = $endpoint;
}
public function setEndPoint($endpoint)
{
$this->endpoint = $endpoint;
}
public function getReceipt()
{
return $this->receipt;
}
public function getReceiptData()
{
return $this->receiptData;
}
public function getEndpoint()
{
return $this->endpoint;
}
public function validate($bundle_id, $transaction_id, $product_code)
{
$http = new \GuzzleHttp\Client([
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'timeout' => 4.0,
]);
$res = $http->request('POST', $this->endpoint, ['body' => $this->receipt]);
$receiptData = json_decode((string) $res->getBody(), true);
$this->receiptData = $receiptData;
switch ($receiptData['status']) {
case 0: // verify Ok
// check bundle_id
if (!empty($receiptData['receipt']['bundle_id'])) {
$receipt_bundle_id = $receiptData['receipt']['bundle_id'];
if ($receipt_bundle_id != $bundle_id) {
throw new \Exception('bundle_id not matched!');
}
}
// check transaction_id , product_id
if (!empty($receiptData['receipt']['in_app'])) {
$in_app = array_combine(array_column($receiptData['receipt']['in_app'], 'transaction_id'), $receiptData['receipt']['in_app']);
if (empty($in_app[$transaction_id])) {
throw new \Exception('transaction_id is empty!');
}
$data = $in_app[$transaction_id];
if ($data['product_id'] != $product_code) {
throw new \Exception('product_id not matched!');
}
} else {
$receipt_transaction_id = $receiptData['receipt']['transaction_id'];
$receipt_product_id = $receiptData['receipt']['product_id'];
if ($receipt_transaction_id != $transaction_id || $product_id != $product_code) {
throw new \Exception('tranaction_id not matched!');
}
}
break;
case 21007:// sandbox order validate in production will return 21007
if ($this->getEndpoint() != self::SANDBOX_URL) {
$this->setEndPoint(self::SANDBOX_URL);
$this->validate($bundle_id, $transaction_id, $product_code);
} else {
throw new \Exception('appstore error!');
}
break;
default:
throw new \Exception("[{$receiptData['status']}]appstore error!");
break;
}
return $receiptData;
}
}
I think Morteza M is correct. I did a test and got reply(JSON) like:
{
'status':
'environment': 'Sandbox'
'receipt':
{
'download_id':
....
'in_app":
{
'product_id':
....
}
....
}
}
Related
I am trying to implement the PayPal IPN solution. I am checking the IPN History page of my saccount and all messages are being sent with a 200 response. The problem is that nothing in my DB is being updated or inserted.
My environment is:
PHP 7.4
Apache
CentOS 8
I also tried to log something to a file but I am failing to do so. This is the first time I try to implement the PayPal IPN and I am very confused.
This is my current code:
<?php
class PaypalIPN
{
/** #var bool Indicates if the sandbox endpoint is used. */
private $use_sandbox = false;
/** #var bool Indicates if the local certificates are used. */
private $use_local_certs = true;
/** Production Postback URL */
const VERIFY_URI = 'https://ipnpb.paypal.com/cgi-bin/webscr';
/** Sandbox Postback URL */
const SANDBOX_VERIFY_URI = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
/** Response from PayPal indicating validation was successful */
const VALID = 'VERIFIED';
/** Response from PayPal indicating validation failed */
const INVALID = 'INVALID';
const DEBUG = true;
/**
* Sets the IPN verification to sandbox mode (for use when testing,
* should not be enabled in production).
* #return void
*/
public function useSandbox()
{
$this->use_sandbox = true;
}
/**
* Sets curl to use php curl's built in certs (may be required in some
* environments).
* #return void
*/
public function usePHPCerts()
{
$this->use_local_certs = false;
}
/**
* Determine endpoint to post the verification data to.
*
* #return string
*/
public function getPaypalUri()
{
if ($this->use_sandbox) {
return self::SANDBOX_VERIFY_URI;
} else {
return self::VERIFY_URI;
}
}
/**
* Verification Function
* Sends the incoming post data back to PayPal using the cURL library.
*
* #return bool
* #throws Exception
*/
public function verifyIPN()
{
if ( ! count($_POST)) {
throw new Exception("Missing POST Data");
}
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
if ($keyval[0] === 'payment_date') {
if (substr_count($keyval[1], '+') === 1) {
$keyval[1] = str_replace('+', '%2B', $keyval[1]);
}
}
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
}
// Build the body of the verification post request, adding the _notify-validate command.
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
$value = urlencode($value);
$req .= "&$key=$value";
}
// Post the data back to PayPal, using curl. Throw exceptions if errors occur.
$ch = curl_init($this->getPaypalUri());
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// This is often required if the server is missing a global cert bundle, or is using an outdated one.
if ($this->use_local_certs) {
curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . "/cert/cacert.pem");
}
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'User-Agent: PHP-IPN-Verification-Script',
'Connection: Close',
));
$res = curl_exec($ch);
if ( ! ($res)) {
$errno = curl_errno($ch);
$errstr = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: [$errno] $errstr");
}
$info = curl_getinfo($ch);
$http_code = $info['http_code'];
if ($http_code != 200) {
throw new Exception("PayPal responded with http code $http_code");
}
curl_close($ch);
// Check if PayPal verifies the IPN data, and if so, return true.
if ($res == self::VALID) {
// crea el LOG
if($this->DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "IPN Verification: $req ". PHP_EOL, 3, 'ipn.log');
}
return true;
} else {
// crea el LOG
if($this->DEBUG == true) {
error_log(date('[Y-m-d H:i e] '). "IPN Verification: $req ". PHP_EOL, 3, 'ipn.log');
}
return false;
}
}
}
$ipn = new PaypalIPN();
// Use the sandbox endpoint during testing.
$ipn->useSandbox();
$verified = $ipn->verifyIPN();
if ($verified) {
/*
* Process IPN
* A list of variables is available here:
* https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNandPDTVariables/
*/
$payment_amount = $_POST['mc_gross'];
$payment_status = $_POST['payment_status'];
$custom = $_POST['custom'];
$payer_email = $_POST['payer_email'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$item_name = $_POST['item_name'];
$payment_currency = $_POST['mc_currency'];
$item_number = $_POST['item_number'];
if($payment_amount >= '20') {
// Donation is $20 or higher, so let's add 20% to the coins.
$coins = $payment_amount*10*1.2;
}
else {
// Donation is less than $20, no bonus.
$coins = $payment_amount*10;
}
// Add E. Coins
$acc = new PDO("sqlsrv:Server=myhost;Database=mydb", "myusr", "mypass");
$add = $acc->prepare("UPDATE CashAccount SET Cash = Cash + :coins WHERE ID = :account");
$add->bindParam(':coins', $coins, PDO::PARAM_INT);
$add->bindParam(':account', $custom, PDO::PARAM_STR);
$add->execute();
// Log the donation
$db = new PDO("sqlsrv:Server=myhost;Database=mydb", "myusr", "mypass");
$method = 'PayPal';
$query = $db->prepare("INSERT INTO logs (Account, Amount, Coins, Method, Date, Email) VALUES (:account, :amount, :coins, :method, GETDATE(), :email)");
$query->bindParam(':account', $custom, PDO::PARAM_STR);
$query->bindParam(':amount', $payment_amount, PDO::PARAM_INT);
$query->bindParam(':coins', $coins, PDO::PARAM_INT);
$query->bindParam(':method', $method, PDO::PARAM_STR);
$query->bindParam(':email', $payer_email, PDO::PARAM_STR);
$query->execute();
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly.
header("HTTP/1.1 200 OK");
I also enbled IPN Notifications in my Business/Sandbox accounts and updated the URL to http://example.com/paypal_ipn.php
Here is my IPN History from the PayPal sandbox.
Maybe someone here can point me to the right direction.
Where have you looked for your ipn.log file?
PHP may not have write permissions to the current directory.
What happens if you turn on errors/warnings and access your IPN listener from a browser? Does it print errors to the screen, about not being able to open ipn.log for writing?
You may need to use an absolute path to a directory PHP does have permissions to write to, for example:
error_log("This is a message!", 3, "/tmp/ipn.log");
I'm trying to write an application to receive notifications via Firebase Cloud Messaging. It initially worked when I tried to send the message via the Firebase console, but then the moment I tried doing the same using PHP webservice, in response, it showed success, but I neither received the message nor am I able to get any more notifications via Firebase console. How can I fix this?
My code in the server side:
<?php
function send_notification ($tokens, $message)
{
$url = 'https://fcm.googleapis.com/fcm/send';
$fields = array(
'registration_ids' => $tokens,
'data' => $message
);
$headers = array(
'Authorization:key = AIzaSyDjrLKZGs4OcIgBPxpFnTzGeZHur4v9V8U',
'Content-Type: application/json'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
$result = curl_exec($ch);
if ($result === FALSE) {
die('Curl failed: ' . curl_error($ch));
}
curl_close($ch);
return $result;
}
$conn = mysqli_connect("localhost:3306", "minaaaa", "tt#2018", "dbSurvey");
$sql = "Select Token From users";
$result = mysqli_query($conn,$sql);
$tokens = array();
if(mysqli_num_rows($result) > 0 ){
while ($row = mysqli_fetch_assoc($result)) {
$tokens[] = $row["Token"];
}
}
mysqli_close($conn);
$message = array("message" => " FCM PUSH NOTIFICATION TEST MESSAGE");
$message_status = send_notification($tokens, $message);
echo $message_status;
?>
Code in Swift:
UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
Util.copyFile(fileName: "db.zanjanwireFinal01.db")
//let alert = UIAlertController(title: title, message: message , preferredStyle: .alert)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]){(isGranted, err)
in
if err != nil {
}
else
{
UNUserNotificationCenter.current().delegate = self
Messaging.messaging().delegate = self
if let token = InstanceID.instanceID().token() {
print("DCS: " + token)
self.RegisterUsers(testStr: token)
}
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
FirebaseApp.configure()
return true
}
func ConnectToFCM() {
Messaging.messaging().shouldEstablishDirectChannel = true
if let token = InstanceID.instanceID().token() {
print("DCS: " + token)
RegisterUsers(testStr: token)
}
}
func RegisterUsers(testStr:String){
let request = NSMutableURLRequest(url: NSURL(string:
request.httpMethod = "POST"
var dataString = "Token=\(testStr)"
print (dataString)
request.httpBody = dataString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest){
data, response, error in
if error != nil {
print("fffffffffatemeeeeeee")
print("error=\(error)")
return
}
print("response = \(response)")
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("responseString = \(responseString)")
}
task.resume()
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to
// inactive state. This can occur for certain types of
// temporary interruptions (such as an incoming phone call
// or SMS message) or when the user quits the application
// and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers,
// and invalidate graphics rendering callbacks. Games
// should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user
// data, invalidate timers, and store enough application
// state information to restore your application to its
// current state in case it is terminated later.
// If your application supports background execution, this
// method is called instead of applicationWillTerminate:
// when the user quits.
//FirstMenueViewController().update()
Messaging.messaging().shouldEstablishDirectChannel = false
}
func applicationWillEnterForeground(_ application: UIApplication) {
FirstMenueViewController().update()
// Called as part of the transition from the background
// to the active state; here you can undo many of the
// changes made on entering the background.
ConnectToFCM()
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started)
// while the application was inactive. If the application
// was previously in the background, optionally refresh
// the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate.
// Save data if appropriate. See also
// applicationDidEnterBackground:.
}
func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
ConnectToFCM()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
UIApplication.shared.applicationIconBadgeNumber += 1
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "com.DouglasDevlops.BadgeWasUpdated"), object: nil)
}
Structure your notification payload like this,
I'm attempting to verify the integrity of a consumable purchase in my app.
I've tested my in app purchase and it works fine. However, upon testing it with an "In-App-Purchase Cracker" with a jailbroken device, I realized that All of my receipts returned ok by apple's servers regardless of whether the purchase actually happened or not.
My transaction listener:
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self unlockFeature];
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
NSLog(#"Transaction Failed");
[[SKPaymentQueue defaultQueue]
finishTransaction:transaction];
break;
default:
break;
}
}
unlockFeature:
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
NSString *rs = [receipt base64EncodedStringWithOptions:kNilOptions];
NSInteger amt = _cDonAmt;
_cDonAmt = 0;
if (receipt) {
NSURL *url = [NSURL URLWithString:#"http://example.com/verifyDonation.php"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:[MasterViewController getUserId] forKey:#"userID"];
[request setPostValue:rs forKey:#"receipt"];
[request setPostValue:[NSString stringWithFormat:#"%ld",(long)amt] forKey:#"dAmt"];
[request setDelegate:self];
[request startAsynchronous];
}
I'm hoping that my error might lie in my PHP Verification script
<?php
class VerifyDonation {
function verify() {
$uid = htmlspecialchars($_POST["userID"]);
$dA = intval(htmlspecialchars($_POST["dAmt"]));
$receipti = $_POST["receipt"];
$sandbox = true;
//start getting
if ($sandbox)
$verify_host = "ssl://sandbox.itunes.apple.com";
else
$verify_host = "ssl://buy.itunes.apple.com";
$json='{"receipt-data" : "'.$receipti.'" }';
//opening socket to itunes
$fp = fsockopen ($verify_host, 443, $errno, $errstr, 30);
if (!$fp)
{
// HTTP ERROR
return false;
}
else
{
//iTune's request url is /verifyReceipt
$header = "POST /verifyReceipt HTTP/1.0\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$header .= "Content-Length: " . strlen($json) . "\r\n\r\n";
fputs ($fp, $header . $json);
$res = '';
while (!feof($fp))
{
$step_res = fgets ($fp, 1024);
$res = $res . $step_res;
}
fclose ($fp);
//taking the JSON response
$json_source = substr($res, stripos($res, "\r\n\r\n{") + 4);
//decoding
$app_store_response_map = json_decode($json_source);
$app_store_response_status = $app_store_response_map->{'status'};
//end geting
if ($app_store_response_status == 0)//eithr OK or expired and needs to synch
{
echo "validReceipt";
$link = mysql_connect("localhost", "user", "pass") or
die("Could not connect: " . mysql_error());
mysql_select_db("database");
mysql_query("UPDATE `users` SET `donarAmt`= donarAmt + $dA WHERE facebookId = $uid");
return true;
}
else
{
echo "invalidReceipt";
return false;
}
}
}
}
// This is the first thing that gets called when this page is loaded
// Creates a new instance of the RedeemAPI class and calls the redeem method
$ver = new VerifyDonation;
$ver->verify();
?>
I have an app that receives a JSON file generated by my jason.php script and displays the data in a table view.
It works fine until I try to use 'include(db_connect.php)' in my jason.php file to pass the database log in details to it.
Running my php script, with 'include(db_connect.php)', does work in a browser (returns the JSON file formatted correctly) but it doesn’t work on my phone.
However..
It does work on my phone if I just paste the contents of db_connect.php into the jason.php file...and it returns exactly the same JSON file in a browser.
Both ways return exactly the same JSON text in browser.
All the app does is expect to receive a JSON file from a specified URL, it does’t pass anything to it. Just visits the URL and stores whats returned in an NSData object.
If anyone knows why this is happening I would be grateful to know!
Thanks
jason.php: this returns a the JSON script perfectly in my browser
<?php
require("db_connect.php");
//Check to see if we can connect to the server
if(!$connection)
{
die("Database server connection failed.");
}
else
{
//Attempt to select the database
$dbconnect = mysql_select_db($db, $connection);
//Check to see if we could select the database
if(!$dbconnect)
{
die("Unable to connect to the specified database!");
}
else
{
$query = "SELECT * FROM cities";
$resultset = mysql_query($query, $connection);
$records = array();
//Loop through all our records and add them to our array
while($r = mysql_fetch_assoc($resultset))
{
$records[] = $r;
}
//Output the data as JSON
echo json_encode($records);
}
}
?>
db_connect.php the log in details
<?php
$host = "xxxxx"; //Your database host server
$db = "xxxxx"; //Your database name
$user = "xxxxx"; //Your database user
$pass = "xxxxx"; //Your password
$connection = mysql_connect($host, $user, $pass);
?>
jason_pasted.php this is exactly the same as jason.php but the contents of db_connect.php are just pasted in - produces exactly the same result in browser, and also works when used in my app.
<?php
$host = "xxxxx"; //Your database host server
$db = "xxxxxx"; //Your database name
$user = "xxxxx"; //Your database user
$pass = "xxxxxx"; //Your password
$connection = mysql_connect($host, $user, $pass);
//Check to see if we can connect to the server
if(!$connection)
{
die("Database server connection failed.");
}
else
{
//Attempt to select the database
$dbconnect = mysql_select_db($db, $connection);
//Check to see if we could select the database
if(!$dbconnect)
{
die("Unable to connect to the specified database!");
}
else
{
$query = "SELECT * FROM cities";
$resultset = mysql_query($query, $connection);
$records = array();
//Loop through all our records and add them to our array
while($r = mysql_fetch_assoc($resultset))
{
$records[] = $r;
}
//Output the data as JSON
echo json_encode($records);
}
}
?>
ViewController.m extract from the app code
-(void) retrieveData
{
NSURL *url = [NSURL URLWithString:jsonURL];
NSData *data = [NSData dataWithContentsOfURL:url];
json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
//set up cities array
citiesArray = [[NSMutableArray alloc]init];
for (int i=0;i<[json count]; i++)
{
//create city object
NSString *cID = [[json objectAtIndex:i] objectForKey:#"id"];
NSString *cName = [[json objectAtIndex:i] objectForKey:#"cityName"];
NSString *cState = [[json objectAtIndex:i] objectForKey:#"cityState"];
NSString *cPopulation = [[json objectAtIndex:i] objectForKey:#"cityPopulation"];
NSString *cCountry = [[json objectAtIndex:i] objectForKey:#"country"];
City *myCity = [[City alloc] initWithCityID:cID
andCityName:cName
andCityState:cState
andCityPopulation:cPopulation
andCityCountry:cCountry];
//add city oject to city array
[citiesArray addObject:myCity];
}
[davesTableView reloadData];
}
TL;DR the app works perfectly with jason_pasted.php but not jason.php.
jason.php and jason_pasted.php return exactly the same JSON script when opened in a browser.
String returned from jason.php and jason_pasted.php
(
{
cityName = London;
cityPopulation = 8173194;
cityState = London;
country = "United Kingdom";
id = 1;
},
{
cityName = Bombay;
cityPopulation = 12478447;
cityState = Maharashtra;
country = India;
id = 2;
},
{
cityName = "Kuala Lumpur";
cityPopulation = 1627172;
cityState = "Federal Territory";
country = Malaysia;
id = 3;
},
{
cityName = "New York";
cityPopulation = 8336697;
cityState = "New York";
country = "United States";
id = 4;
},
{
cityName = Berlin;
cityPopulation = 3538652;
cityState = Berlin;
country = Deutschland;
id = 5;
}
)
error returned only when NSUrl points to jason.php
2014-02-13 11:43:34.760 JSONios[4655:60b] JSON error: Error Domain=NSCocoaErrorDomain Code=3840
"The operation couldn’t be completed. (Cocoa error 3840.)" (JSON text did not start with array or
object and option to allow fragments not set.)
UserInfo=0x14659c40 {NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
This is placed in an answer for formatting:
Do not ignore errors!
Incorrect:
json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
Note: The docs do not specify that nil may be passed for the error parameter.
Correct:
// There is an assumption that the JSON head is a dictionary.
NSError *error;
NSDictionary *jsonAsDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (jsonAsDict == nil) {
NSLog(#"JSON error: %#", error);
}
else { // For debug only
NSLog(#"JSON: %#", jsonAsDict);
}
Now, what happens with this code?
Also please provide the JSON string if possible.
Oh, I personally to not care how the php creates the JSON, all I need to see is the JSON.
Still having trouble: NSLog the data as a string:
NSLog(#"data: %#", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
If no data add the error parameter to
dataWithContentsOfURL:options:error:
in place of dataWithContentsOfURL:
My iPhone app communicates through a php file with my mySql database. Everything works fine. But when I send the form and the username already exists, the php file should somehow backfire a notification. What should I do ?
php File
$query = "SELECT username FROM userData WHERE username = '$username'";
$result = mysql_query($query);
if (mysql_num_rows($result) > 0) {
// WHAT SHOULD BE DONE HERE TO NOTIFY iPHONE ?
}
I could think of a way, but it I think there is a better and more efficient way to do it.
[EDIT]
This is what I did to get the response:
NSURLResponse *theResponse =[[NSURLResponse alloc]init];
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&theResponse error:&error];
NSDictionary *jsonDictionaryResponse =[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
NSLog(#"json response = %#", jsonDictionaryResponse);
[/EDIT]
He does not really need a push notification. Just need a response back from the server.
$query = "SELECT username FROM userData WHERE username = '$username'";
$result = mysql_query($query);
if (mysql_num_rows($result) > 0) {
// WHAT SHOULD BE DONE HERE TO NOTIFY iPHONE ?
sendResponse(200, json_encode('SUCCESS Notification'));
}
Where sendResponse looks like this
// Helper method to send a HTTP response code/message
function sendResponse($status = 200, $body = '', $content_type = 'text/html')
{
$status_header = 'HTTP/1.1 ' . $status . ' ' . 'OK';
header($status_header);
header('Content-type: ' . $content_type);
echo $body;
}