I'm trying to add Beeblex's new In App Purchase verification to my app, however i'm struggling passing a return value from within a block.
Here's the code I have now, and as you can see I set a BOOL value, then within the verification block I set the BOOL and return it at the end. However the return at the end is called before the block finishes, so what I need is to return the BOOL from within the block?
- (BOOL)verifyTransaction:(SKPaymentTransaction *)transaction
{
if (![BBXIAPTransaction canValidateTransactions]) {
return YES; // There is no connectivity to reach the server
}
BOOL __block toReturn = YES;
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
NSLog(#"Transaction is a duplicate!");
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
toReturn = NO;
} else {
// The transaction has been successfully validated and is unique
NSLog(#"Transaction valid data:%#",bbxTransaction.validatedTransactionData);
[FlurryAnalytics logEvent:#"Transaction verified"];
toReturn = YES;
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
NSLog(#"Transaction error caused by network, not data");
[FlurryAnalytics logEvent:#"Transaction network error"];
toReturn = YES;
} else {
// The transaction supplied to the validation service was not valid according to Apple
NSLog(#"Transaction not valid according to Apple");
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
toReturn = NO;
}
}
}];
NSLog(#"toReturn: %#",toReturn ? #"Yes" : #"No");
return toReturn;
}
If I simply put return = NO; inside the block, I get compiler warnings of Incompatible block pointer types, and control may reach end of non-void block.
Beeblex developer here. The code inside -validateWithCompletionBlock: execute asynchronously (in the background, it needs to talk to our servers, so there's no point blocking your app completely while we wait for the Internet to do its thing).
Therefore, you need to rethink your approach to validating your receipts. Right now you, general workflow is:
Complete purchase
Call Beeblex
Wait for response
Check boolean value
But #3 returns right away, so this will never work. You should consider doing something like this:
Complete purchase
Show a “Please wait…” view, or something similar that advises the user that you're unlocking whatever they've purchased.
Call Beeblex
Inside the block, determine whether the validation succeeded or not, and then act to unlock the content from there.
Sit idle until called by the block
Here's a quick-and-dirty example. I didn't compile it, so it probably has a few bugs, but it should give you the idea behind the intended usage pattern.
- (void) showWaitView {
// Display a wait view
}
- (void) hideWaitView {
// Hide the wait view
}
- (void) completeValidationWithValidateReceiptData:(NSDictionary *) receipt isRecoverableError(BOOL) isRecoverableError {
[self hideWaitView];
if (receipt) {
// Unlock the content, tell the user
} else {
if (isRecoverableError) {
// Probably a network error of some kind. Tell user they need to be connected,
// and ask them to do it again.
} else {
// Keep the content locked, tell the user something went wrong
}
}
}
- (void) validateReceipt:(SKPaymentTransaction *) transaction {
if (![BBXIAPTransaction canValidateTransactions]) {
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:YES];
return;
}
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:NO];
} else {
// The transaction has been successfully validated and is unique
[FlurryAnalytics logEvent:#"Transaction verified"];
[self completeValidationWithValidateReceiptData:bbxTransaction.validatedTransactionData isRecoverableError:NO];
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
[FlurryAnalytics logEvent:#"Transaction network error"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:YES];
} else {
// The transaction supplied to the validation service was not valid according to Apple
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
[self completeValidationWithValidateReceiptData:Nil isRecoverableError:NO];
}
}
}];
}
<3 blocks
- (void)verifyTransaction:(SKPaymentTransaction *)transaction completionHandler:(void (^)(BOOL flag))completionHandler
{
if (![BBXIAPTransaction canValidateTransactions]) {
completionHandler(YES); // There is no connectivity to reach the server
}
BBXIAPTransaction *bbxTransaction = [[BBXIAPTransaction alloc] initWithTransaction:transaction];
bbxTransaction.useSandbox = YES;
[bbxTransaction validateWithCompletionBlock:^(NSError *error) {
if (bbxTransaction.transactionVerified) {
if (bbxTransaction.transactionIsDuplicate) {
// The transaction is valid, but duplicate - it has already been sent to Beeblex in the past.
NSLog(#"Transaction is a duplicate!");
[FlurryAnalytics logEvent:#"Transaction duplicate!"];
completionHandler(NO);
} else {
// The transaction has been successfully validated and is unique
NSLog(#"Transaction valid data:%#",bbxTransaction.validatedTransactionData);
[FlurryAnalytics logEvent:#"Transaction verified"];
completionHandler(YES);
}
} else {
// Check whether this is a validation error, or if something went wrong with Beeblex
if (bbxTransaction.hasConfigurationError || bbxTransaction.hasServerError || bbxTransaction.hasClientError) {
// The error was not caused by a problem with the data, but is most likely due to some transient networking issues
NSLog(#"Transaction error caused by network, not data");
[FlurryAnalytics logEvent:#"Transaction network error"];
completionHandler(YES);
} else {
// The transaction supplied to the validation service was not valid according to Apple
NSLog(#"Transaction not valid according to Apple");
[FlurryAnalytics logEvent:#"Transaction invalid!!"];
completionHandler(NO);
}
}
}];
}
Then use
[instance verifyTransaction:transaction completionHandler:^(BOOL flag) {
if (flag) {
// YOUR CODE HERE
}
}];
instead of
if ([instance verifyTransaction:transaction]) {
// YOUR CODE HERE
}
Related
I'm writing a class to centralize remote calls to my backend.
The calls have to be authenticated with oauth, so in each methods, I first check if my token is valide, if not, I request a refreshed one.
Each method looks like this:
-(void)getRemoteDataAndRun:(nullable void(^)(NSDictionary * __nullable json))success
orFail:(nullable void(^)(AFHTTPRequestOperation * __nullable operation, NSError * __nullable error))failure {
[self checkOAuthTokenAndRun:^{
// my remote call
...
success(json);
} orFail:failure];
}
the method checkOAuthTokenAndRun check the token validity and request a new one if needed.
My problem is that I have to ensure that checkOAuthTokenAndRun is not called concurrently to prevent reads of credentials while it's refreshed (written).
I tried with NSLock but with blocks (multithreading), I get the following error:
*** -[NSLock lock]: deadlock (<NSLock: 0x7ffd9313d9b0> '(null)')
Here is the complete checkOAuthTokenAndRun method:
- (void)checkOAuthTokenAndRun:(void (^)())action orFail:(nullable void(^)(AFHTTPRequestOperation * __nullable operation, NSError * __nullable error))failure
{
NSLog(#"lock oauth check/refresh");
[oauthLock lock];
if (credential == nil || [credential isExpired])
{
NSLog(#"Credentials are null or expired");
if (credential == nil || credential.refreshToken == nil)
{
NSLog(#"Credentials are null or anonymous+expired");
[self getAnonymousCredentialsAndRun:^{
NSLog(#"unlock oauth");
[oauthLock unlock];
action();
} orFail:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nullable error) {
NSLog(#"unlock oauth");
[oauthLock unlock];
failure(operation, error);
}];
}
else
{
NSLog(#"Credentials are expired");
AFOAuth2Manager *OAuth2Manager = [[AFOAuth2Manager alloc] initWithBaseURL:[NSURL URLWithString:BaseURL] clientID:clientId secret:clientSecret];
[OAuth2Manager authenticateUsingOAuthWithURLString:#"/oauth/v2/token" refreshToken:credential.refreshToken success:^(AFOAuthCredential *_credential) {
credential = _credential;
NSLog(#"Received refreshed token");
[AFOAuthCredential storeCredential:credential
withIdentifier:OAuthProviderIdentifier];
NSLog(#"unlock oauth");
[oauthLock unlock];
action();
} failure:^(NSError *error) {
NSLog(#"Failed refreshing oauth token (%#)", [error localizedDescription]);
// remove refresh token
NSLog(#"Unable to get oauth token, delete credentials (%#)", [error localizedDescription]);
[AFOAuthCredential deleteCredentialWithIdentifier:OAuthProviderIdentifier];
credential = nil; //[AFOAuthCredential retrieveCredentialWithIdentifier:OAuthProviderIdentifier];
NSLog(#"unlock oauth");
[oauthLock unlock];
failure(nil, error);
}];
}
}
else { // run the action
NSLog(#"Credentials are valid (%#)", (credential.refreshToken.length ? #"refresh token defined" : #"refresh token not defined"));
NSLog(#"unlock oauth");
[oauthLock unlock];
action();
}
}
As you see, I'm using nested blocks, I don't know if this is the reason why I get the deadlocks.
Once again, I only want to prevent concurrent execution of checkOAuthTokenAndRun method
Thanks for your help
Network call are concurrent and they have to be concurrent. The problem with your code is you called a concurrent method inside another method which is itself concurrent. Solution to your problem:
Call method checkOAuthTokenAndRun first and if success(response from block) then call method getRemoteDataAndRun.
[self checkOAuthTokenAndRun:^{
if(success){
[self getRemoteDataAndRun:^{
}]
}
} orFail:failure];
//Above code snippet is just a example to solve your problem. You need redesign the blocks parameter
I am using RMStore to verify receipts. As a note, I am not using RMStore for the actual purchase portion. The process is successfully dealing with success and failure in terms of throwing errors and not delivering content if the receipt is invalid. I purposefully changed the bundle to force a failure as a test. My question though is with the failure process and the confirmation Apple sends.
The issue is that while this process does detect the failure to verify and therefore does prevent the content from being sent to the user, Apple still afterwards comes back with a dialog box about the purchase being successful. The good news is that the purchase isn't successful and the content isn't delivered, but I would prefer that this dialog box from Apple not show as it will create confusion.
Here is my implementation of the check. For now I am just testing the failure scenario before doing more within the failure block.
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
NSLog(#"completeTransaction...");
RMStoreAppReceiptVerificator *verifyReceipt = [[RMStoreAppReceiptVerificator alloc]init];
[verifyReceipt verifyTransaction:transaction success:^{
[self provideContentForProductIdentifier:transaction.payment.productIdentifier];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}failure:^(NSError *error){
NSLog(#"failure to verify: %#",error.description);
}];
}
Is there a way within the failure block to halt the process at Apple that creates their success dialog or do I need to perform this check at an earlier stage?
Update:
In looking further at this, the method above is being called by the state SKPaymentTransactionStatePurchased The definition of that state per Apple is:
"The App Store successfully processed payment. Your application should provide the content the user purchased."
This tells me that it is likely too late to prevent the dialog. There is an earlier state, however, I would think the receipt verification has to come after purchase but before delivery of content (otherwise there would not be a purchase to verify). So is this just a matter of having to deal with the conflicting message or am I missing something?
Update 2: Adding some more methods per the request in comments
#interface IAPHelper () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
#end
#implementation IAPHelper
{
SKProductsRequest * _productsRequest;
RequestProductsCompletionHandler _completionHandler;
NSSet * _productIdentifiers;
NSMutableSet * _purchasedProductIdentifiers;
NSDictionary *_mappingDict;
}
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers andMappings:(NSDictionary *)mappingDict
{
if ((self = [super init])) {
// Store product identifiers & mappings
_productIdentifiers = productIdentifiers;
_mappingDict = mappingDict;
// Add self as transaction observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)requestProductsWithCompletionHandler:(RequestProductsCompletionHandler)completionHandler {
// 1
_completionHandler = [completionHandler copy];
// 2
_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers];
_productsRequest.delegate = self;
[_productsRequest start];
}
- (BOOL)productPurchased:(NSString *)productIdentifier {
return [_purchasedProductIdentifiers containsObject:productIdentifier];
}
- (void)buyProduct:(SKProduct *)product {
NSLog(#"Buying %#...", product.productIdentifier);
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSLog(#"Loaded list of products...");
_productsRequest = nil;
NSArray * skProducts = response.products;
for (SKProduct * skProduct in skProducts) {
NSLog(#"Found product: %# %# %0.2f",
skProduct.productIdentifier,
skProduct.localizedTitle,
skProduct.price.floatValue);
}
if (_completionHandler)
{
_completionHandler(YES, skProducts);
_completionHandler = nil;
}
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
NSLog(#"Failed to load list of products.");
_productsRequest = nil;
if (_completionHandler)
{
_completionHandler(NO, nil);
_completionHandler = nil;
}
}
Here is the specific method that calls completeTransaction
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
default:
break;
}
};
}
We established in the comments that this question doesn't have anything to do with RMStore.
You're not testing a real fraud scenario so it doesn't matter if Apple shows an alert.
This would involve either using a fake receipt, or sending fake calls to the Store Kit transaction observer. In neither of those cases you would get the alert.
When using a valid transaction to simulate a failure scenario you can't expect Apple to consider the transaction invalid as well. There is no API to tell Apple that a transaction is fraudulent. You can only finish a transaction, or not.
I've working on/learning this all afternoon.
Following an example here: http://themainthread.com/blog/2012/09/communicating-with-blocks-in-objective-c.html, I have managed to setup a callback to get the result of an asynchronous call to a web service i have.
The web service takes a key code and the app transforms it and passes it back for authentication.
With my code below, how can I change the method from a void to an NSString that I can call to return my pass code?
-(void) showPassCode{
getAuthCodeAndMakePassCodeCompleteBlock callback = ^(BOOL wasSuccessful, NSString *passCode) {
if (wasSuccessful) {
NSLog(#"Code is: %#", passCode);
} else {
NSLog(#"Unable to fetch code. Try again.");
}
};
[self getAuthCodeAndMakePassCode:#"myAuthCode" withCallback:callback];
}
Ideally, I want it to work or look like this:
-(NSString *) strPassCode{
getAuthCodeAndMakePassCodeCompleteBlock callback = ^(BOOL wasSuccessful, NSString *passCode) {
if (wasSuccessful) {
return passCode;
} else {
return nil;
}
};
[self getAuthCodeAndMakePassCode:#"myAuthCode" withCallback:callback];
}
Without knowing the specifics of your code and how you query the server, I have to imagine it would look something like:
-(void)getAuthCodeWithCallback:(void (^)(NSString* authCode))callback
{
//make server call, synchronously in this example
NSString* codeReturnedFromServer = [self getServerCodeSynchronous];
callback(codeReturnedFromServer);
}
//some calling code
[self getAuthCodeWithCallback:^(NSString* authCode) {
NSLog(#"Code is: %#", authCode);
}];
If the method that gets your auth code from the server is asynchronous, it would look something like this:
-(void)getAuthCodeWithCallback:(void (^)(NSString* authCode))callback
{
//make server call, asynchronously in this example
[self someMethodCallToQueryCodeFromServerWithCallback:^(NSError* error, NSString* code) {
if (error) {
//handle error
}
else
callback(code);
}
}
I have internet connection and can browsing with browser.
Here is my codes to check Reachability with AFNetworking.
- (BOOL)connected {
return [AFNetworkReachabilityManager sharedManager].reachable;
}
And In ViewDidLoad
BOOL isOnline = [self connected];
if(isOnline == YES)
{
NSLog(#"YES");
}
else
{
NSLog(#"NO");
}
It's only showing NO and i don't know why is it?
Is there easiest way to check Reachability with AFNetworking?
I guess startMonitoring isn't called, try to do the below:
- (void)viewDidLoad {
[super viewDidLoad];
....
[[AFNetworkReachabilityManager sharedManager] startMonitoring];
}
If above answer is not solving your issue,
then your problem might be due to calling [AFNetworkReachabilityManager sharedManager].reachable while it is in the middle of 'startMonitoring' process where it would always return NO.
I had the same issue. I was calling web service while AFNetworkReachabilityManager had not finished monitoring process and was returning reachable = NO although I had working internet connection.
- (void) callWebService {
NSLog(#"Calling Webservice...");
if ([AFNetworkReachabilityManager sharedManager].reachable == NO) {
NSLog(#"%#", kErrorNoInternet);
return;
}
// Now, proceed to call webservice....
}
So, to solve this I did a trick. Called web service after some delay (in this example 0.05 sec).
Before:
[self callWebService];
Output:
After:
[self performSelector:#selector(callWebService) withObject:nil afterDelay:0.3]; // you can set delay value as per your choice
Output:
You can see from the output, the time difference is hardly 0.05 sec (exact value 0.048 sec).
Hope this will help.
instead of waiting you can use blocks just to make sure that your web service will be only called when network is available.
[[AFNetworkReachabilityManager sharedManager]startMonitoring];
[[AFNetworkReachabilityManager sharedManager]setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status)
{
if (status == AFNetworkReachabilityStatusReachableViaWWAN || status == AFNetworkReachabilityStatusReachableViaWiFi)
{
// connected. you can call your web service here
}else
{
// internet disconnected
}
}];
I am wondering how I can prevent a crash when trying to close UIDoc twice. I tried to make sure in my code that you (theoretically) can not close a UIDocument twice. However, it still happens sometimes and I don't know why. If it does, the app crashes:
2012-07-06 15:24:34.470 Meernotes[11620:707] ... doc state:Normal
2012-07-06 15:24:34.472 Meernotes[11620:707] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'closeWithCompletionHandler called while document is already closing'
*** First throw call stack:
(0x3720e88f 0x34f13259 0x3720e789 0x3720e7ab 0x312681d1 0xd19db 0x96f7f 0x9593f 0xacb8f 0x30f0cd23 0x37a7f933 0x371e2a33 0x371e2699 0x371e126f 0x371644a5 0x3716436d 0x33923439 0x30f10cd5 0x94fdd 0x94f78)
terminate called throwing an exception(lldb)
I tried to prevent the crash as follows, but it has no effect whatsoever (i.e. it will still crash):
-(void)closeDoc {
UIDocumentState state = _selectedDocument.documentState;
NSMutableArray * states = [NSMutableArray array];
if (state == 0) {
[states addObject:#"Normal"];
}
if (state & UIDocumentStateClosed) {
[states addObject:#"Closed"];
}
if (state & UIDocumentStateInConflict) {
[states addObject:#"In conflict"];
}
if (state & UIDocumentStateSavingError) {
[states addObject:#"Saving error"];
}
if (state & UIDocumentStateEditingDisabled) {
[states addObject:#"Editing disabled"];
}
NSLog(#"... doc state: %#", [states componentsJoinedByString:#", "]);
if (_selectedDocument.documentState & UIDocumentStateClosed) return;
[_selectedDocument closeWithCompletionHandler:^(BOOL success) {
NSLog(#"Closed document.");
// Check status
if (!success) {
NSLog(#"Failed to close %#", _selectedDocument.fileURL);
} else {
_selectedDocument = nil;
}
}];
}
It looks like UIDocument doesn't store the closing state, only normal and closed, so you'll have to do it yourself.
Add this to your class variables:
BOOL _documentClosing;
And add its use in your closeDoc method:
-(void)closeDoc {
if (_docClosing || (_selectedDocument.documentState & UIDocumentClosed) != 0)
return;
_docClosing = YES;
[_selectedDocument closeWithCompletionHandler:^(BOOL success) {
NSLog(#"Closed document.");
// Check status
if (!success) {
NSLog(#"Failed to close %#", _selectedDocument.fileURL);
} else {
_selectedDocument = nil;
_docClosing = NO;
}
}];
}
It's very important to know that each UIDocument object can only be opened and closed once. I had a lot of odd problems with UIDocuments before realizing this, both in the cloud and with local files. When you close a document set its pointer to nil, so you can't close it again. If you need to access the same file again later, create a new UIDocument with the same fileURL.
The error message you show above is one that appears when you try to re-use a document.