Detecting user canceling SKReceiptRefreshRequest to sign in - ios7

When we need to refresh an iOS7 receipt, the appropriate way to do that is to use SKReceiptRefreshRequest. This brings up an Apple dialog for the user to sign in using their Apple ID. It also allows them to Cancel. How can we detect if the user presses Cancel? (Of course, in iOS6, this can be done using catching the cancel event for [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]).
The SKReceiptRefreshRequest object has a delegate with two methods:
- (void)requestDidFinish:(SKRequest *)request;
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error;
In my code the requestDidFinish delegate method is called if the user does or does not press cancel.
The reason this is important to me is that I want to cancel the restore process if the user presses Cancel. If there was simply no receipt present after the refresh request, this might be relatively easy. However, sometimes there is a receipt (with some purchases) present in the app before the SKReceiptRefreshRequest, and so it stays in the app if the user cancels from the sign-in dialog.
I have two ideas for how to do this:
1) Delete the receipt from the bundle prior to the refresh request. The apparent problem with this is that the app can't remove files from the bundle (e.g., see Delete file from bundle after install). I tried. Nope.
2) Check the bytes of the receipt before the refresh request and after; if they differ, then this should indicate the user didn't press Cancel. If they don't differ, I'm not sure that does indicate they pressed Cancel. If the receipt contains purchases, I think that the bytes will differ because a refreshed receipt should have different transaction id's (but the same "original" transaction id's). If the receipt contains no purchases, I'm not sure about that.
Update, 11/9/15; I've just noticed that the delegate response to the user pressing Cancel appears to have changed. Now, didFailWithError is called. The problem of detecting user cancellation remains, however. How can we distinguish between the user pressing Cancel and a genuine error? I've been testing on iOS8.4 and iOS9.2 (beta). I have now reported this lack of distinguishability as a bug to Apple (bug #23476210).
Update, 11/10/15; This issue does not appear with iOS 9.0.2! I just tried this now with all three systems, with the same app binary, in the same approximate interval of time (within 20 minutes for all three systems): (A) iOS9.2 (13C5050d): Issue does occur (didFailWithError is called and can't distinguish between a real error and user pressing cancel), (B) iOS9.0.2, issue does not occur (requestDidFinish is called), and (C) iOS8.4.1, issue does occur. With all three system versions, this is running on real hardware, not the simulator.

iOS 9.2.1, Xcode 7.2.1, ARC enabled
I understand you asked this 2 years ago, but I recently had my run in with this issue and found a solution, and wanted to share here so others can save some time.
1) You may elect to store the receipt into keychain and access a copy of it, this allows you to delete it or refresh it as you see fit.
2) Yeah you can definitely check if it changed, I think the simplest way to do that is to use:
[receipt isEqualToData:(NSData *)(copyReceiptObject)]
My suggestion is as follows:
The key is to expect the error you get from the method:
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error;
that gets called when the user taps cancel after the log in dialogue comes up to sign in to iTunes Store. There are many custom was you could do the refresh request, so the results may vary, but here is the different error you get for my request:
When there is no connection to iTunes:
Error Domain=SSErrorDomain Code=110 "Cannot connect to iTunes Store"
UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store,
NSUnderlyingError=0x13c76d680 {Error Domain=NSURLErrorDomain
Code=-1009 "Cannot connect to iTunes Store"
UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store,
NSErrorFailingURLStringKey=
{ your product ids and corresponding URIs here }
, _kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12,
NSLocalizedDescription=The Internet connection appears to be offline.}}}
When the user taps cancel:
Error Domain=SSErrorDomain Code=16 "Cannot connect to iTunes Store"
UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store,
NSUnderlyingError=0x13c6ac7b0 {Error Domain=AKAuthenticationError
Code=-7003 "(null)"}}
The easiest I think is to check the error.code, if you want to get more complicated you may choose to parse the full error string to get more details about the error.
In my case, no connection error code is 110 and when the user cancels the log in the error code is 16. Handle the error.

Related

Confirm purchase order from automated action in Odoo

I created an automated action in Odoo that is triggered when a purchase order is updated from state = draft to state = sent. This automated action simply calls a server action that runs the following code:
record.button_confirm()
The idea is I am trying to confirm the purchase order automatically after the RFQ is sent to the vendor.
After sending the RFQ the PO is confirmed as expected. However, the vendor on the PO also receives a notification from the system related to the picking that was created. It will say something like:
"This transfer has been created from: PO00007"
If I disable my automated action and instead confirm the PO manually by clicking the "Confirm Order" button on the PO form, the order is approved, a picking is created, and this notification is not sent.
I would like my automatic approval to mimic the manual approval. Meaning, I do not want this notification to be sent.
After searching the source code, I was able to accomplish what I wanted by passing in a context key like this in the server action:
record.with_context(message_create_from_mail_mail=True).button_confirm()
I came across this solution here in the source code:
https://github.com/odoo/odoo/blob/df60828488a702acb64757f518b4be12b7a95a04/addons/mail/models/mail_message.py#L729
However, I don't understand why this is necessary or if it's even the correct way to solve this problem. I honestly don't even know what it means. I just know it works. I don't see the manual "button_confirm" method passing in this context key anywhere.
Can anyone offer any guidance here as to why this notification email is sent when I simply run button_confirm() from a server action rather than triggering it manually with a button from the UI? Also, is my solution correct or does it have some sort of side effect I am not aware of?

Values not always persisted in App group between companion app & app extension

From time to time, but not always (I have had this working for a bit), the app/extension gets in a state where I can't read a flag set in my App Group between my companion app and my app extension. Don't know how it gets in this state or why the values differ, but it's critical to my application these always be in sync.
Companion app viewDidLoad:
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
.....
[myAppSettings setBool:true forKey:#"myBool"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"Companion app - bool TRUE" : #"Companion app - bool FALSE");
App extension viewDidLoad
NSUserDefaults *myAppSettings = [[NSUserDefaults alloc] initWithSuiteName:#"group.myapp"];
[myAppSettings synchronize];
NSLog([myAppSettings boolForKey:#"myBool"] ? #"App extension app - bool TRUE" : #"App extension - bool FALSE");
Console output
Companion app - bool TRUE
App extension - bool FALSE
I also synchronize before my companion app will enter background. I have my app group set up in the portal etc.
What am I doing wrong?
EDIT
Apparently others having this problem too:
https://devforums.apple.com/message/977151#977151
"I think that this is currently very glitchy.
Sometimes the data sharing works, then a change and all of a sudden the widget can't see the shared data anymore (both on Simulator and device).
Annoying and hope it's a bit more reliable in next beta!"
EDIT 2
Looks like another person has reported this exact issue as well:
"I also noticed the same thing too.This not only happen to the
NSUserDefaults, but also all the files in the container folder. The
keyboard extension suddenly will lose read/write pemission to the
container folder after using the keyboard for a while."
EDIT 3
More evidence: https://devforums.apple.com/message/1028078#1028078
After I upgrade to beta 3, I noticed that sometimes the keyboard
failed to open the database because it failed to access to the DB
file. The keyboard has been able to access to the file before.
EDIT 4
Seems like this could be because the keyboard loses the RequestsOpenAccess flag. But I can't reproduce it, and there's no way for me to tell for sure.
EDIT 5
Seems like others are reporting this in the iOS8 GM build:
This issue still persists for me in the GM. It seems related to a
keyboard crash.. but also there seems to be some contention between
keyboard and containing app in terms of who creates the suite in what
order. I think this problem is on Apple's end. Trust me, I WANT it to
be my fault but I've spent countless hours with trial and error. No
matter what I do in code and verify with NSLog, it will end up in this
state eventually. Hoping someone finds a magic pill. :S
Has anyone solved this yet?
You must request open access in order to access shared NSUserDefaults. It's stated directly in the App Extension Programming guide:
By default, a keyboard has no network access and cannot share a container with its containing app. To enable these things, set the value of the RequestsOpenAccess Boolean key in the Info.plist file to YES.
Be sure you change the RequestsOpenAccess field to YES. You'll find it in keyboard's Info.plist > NSExtension > NSExtensionAttributes > RequestOpenAccess. Then remove the keyboard in Settings, delete the app, run it again, and add the keyboard again. After you add it, tap on the keyboard name and then flip the switch to enable Allow Full Access. You'll need to instruct the users to follow those same steps to grant access (and reassure them you're not evil), otherwise it simply will not work and you'll never get the data that's stored in your shared container. Note that in iOS 8.3+, if the user hasn't enabled full access the keyboard will be able to access the shared container, but writing to it will not save the data, for security and privacy purposes. In 8.2- you can't access that data without open access granted.
I can confirm that the problem is related to RequestsOpenAccess flag.
Assuming that everything done right (NSUserDefaults use initWithSuiteName, all Capabilities for main application and custom keyboard were set, etc.) I have the next steps:
1) Install the main application and a custom keyboard on device
2) Set 'Allow full access' for the custom keyboard to YES
3) Add some items (in my case this is a simple text templates) in the main app
4) Go to keyboard and check that all items, that were added from the main app,
appeared in custom keyboard
5) Go to main app and add a few more items
6) Go to keyboard and now you will see that nothing changed
7) Go to settings and switch 'Allow full access' to NO and then to YES
8) Go to custom keyboard again and check that item which were added in step 5 appeared.

restoreCompletedTransactions never calls updatedTransactions in StoreKit

I've been banging my head against the wall for a few days with this, since everything used to work fine, but now that I've moved to Mountain Lion, XCode 4.5 and iOS5.1 and iOS6, this issue came along and I thought it might be related to the configuration switch.
I call [[SKPaymentQueue defaultQueue] restoreCompletedTransactions] and moments later paymentQueueRestoreCompletedTransactionsFinished is called, but there is no sign of any call to updatedTransactions. It's like the request got lost in space.
I'm also having a possibly related problem with purchases. If I try to repurchase an item using makePurchase, which from what I understand should also lead to a SKPaymentTransactionStateRestored, I get the "Already purchased.. download" message followed by the dreaded "Cannot connect to iTunes store..." with a "PaymentTransactionStateFailed" error code 2. What is error code 2?
The item is a normal non-consumable in-app puchase, and this happens when testing the StoreKit in sandbox mode.
I'm seeing others on this forum with similar issues with the only reply being that this is an intermittent bug i the StoreKit. Is this still the most plausible case?
Any news on this would be appreciated.
If you clear the purchase history more info here, or the user has never made a purchase before. paymentQueue:updatedTransactions will never be called. You need to add paymentQueueRestoreCompletedTransactionsFinished. Also to be sure check
queue.transactions.count == 0
All code here:
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
print("paymentQueueRestoreCompletedTransactionsFinished",queue.transactions.count)
if queue.transactions.count == 0 {
//call FailedNotification
}
}
paymentQueue:updatedTransactions: is only called when the state of transactions has changed.
If there are no previous transactions [[SKPaymentQueue defaultQueue] restoreCompletedTransactions] will not change the state of these transactions (because there are none) so paymentQueueRestoreCompletedTransactionsFinished is called without a call to paymentQueue:updatedTransactions:
I'm also having a possibly related problem with purchases. If I try to repurchase an item using makePurchase, which from what I understand should also lead to a SKPaymentTransactionStateRestored, I get the "Already purchased.. download" message followed by the dreaded "Cannot connect to iTunes store..." with a "PaymentTransactionStateFailed" error code 2. What is error code 2?
You need to call [[SKPaymentQueue defaultQueue] finishTransaction:] after handling a successful transaction.

libPusher + updating an open NSMenu

I have an NSMenu that I want to update with items pushed to my app through pusherapp and received using the libPusher client library. But events seem not to be received in NSEventTrackingRunLoopMode.
Given the following snippet:
[channel bindToEventNamed:#"my_event" handleWithBlock:^(PTPusherEvent *event) {
NSLog(#"event received");
}];
And I wait for a push to occur while I maintain the menu open, I expect to receive the event immediately but I only receive it when I close the menu.
I also tried passing the main queue to bindToEventNamed:handleWithBlock:queue: (using dispatch_get_main_queue();), to no avail.
So I'm left wondering whether I'm doing something wrong or there's a bug in libPusher?
I'm the author of libPusher. The reason you are seeing this problem is because the underlying WebSocket library used by libPusher, SocketRocket only works in the default run loop mode.
The good news is that this has been fixed in the latest HEAD of SocketRocket. I have tested libPusher agains the latest SocketRocket and can confirm that it fixes this issue and I intend to roll these changes in to the next release.
Now, I've just checked the outstanding Github issue and realised that you were the original reporter of this bug, so you probably know all this already but I'm going to post this answer anyway for posterity.
libPusher issue
SocketRocket issue

How do I pause my app until a crash report has been submitted?

Background
I'm using UKCrashReporter in my app.
I've installed my own Uncaught
Exception Handler.
I'm setting up the
managedObjectContext of the object
activeItemController in
applicationDidFinishLaunching (1)
The Problem
If the managedObjectContext method throws an exception, the crash reporter dialog box only flashes up before the app crashes and so the user never gets to report the crash.
I want my app to continue only after the crash has been reported, not whilst the window is showing.
What I've tried
If UKCrashReporterCheckForCrash()
were an objective C method, I assume
I could call
performSelectorOnMainThread:waitUntilDone:YES
but it's not.
I've looked at some other Stack
Overflow questions about using
Conditional Locks to pause apps,
but I can't understand how I'd use it
for a C function.
How would I go about doing this in a nice way? Do people have any advice
for me? Any responses would be much
appreciated.
The Code
// In app delegate
-(void)applicationWillFinishLaunching:(NSNotification *)aNotification {
UKCrashReporterCheckForCrash(); // A C function which then creates a window if
// it detects a crash has happened.
}
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[activeItemController setMoContextDisk:[self managedObjectContext]];
[activeItemController setMoContextMemory:[self managedObjectContextMemory]];
}
Update 1
I've been asked for more details on what I'm trying to do, so here goes.
The bug that triggered this thinking was an exception when merging managedObjectModels. My app got caught in a loop printing "Uncaught exception" to the console every few milliseconds.
And when I installed the uncaught exception handler before this exception happened, I'd get the described behaviour - my app would fire up, display the crash report dialog briefly, then continue to load and crash again.
Summary - I want to be able to handle errors that happen on startup.
(1) I'm not using bindings to do this, as I thought bindings would make testing the class more problematic.
I think your problem is with thinking of it as "pausing" your app. Think of it more as a different initial UI state. Your attempts to block the run loop will prevent any interactive window from ... well, being interactive. :-)
Your best bet is to show your main UI (and connect data sources, etc) only if the "am I prompting the user to submit a crash report" method says "no, go ahead and start normally". Otherwise, show your window and, when the user sends or declines to send the report, close the window and ask your app controller to continue the normal startup.
I looked at UKCrashReporterCheckForCrash() and it doesn't appear to create a window of any kind. It merely submits the crash. Could you describe what you're doing with more detail?