CKModifyBadgeOperation is deprecated in iOS 11. Anyone know an alternative approach? - ios11

I have searched, and I cannot find an example. I have also tried adapting this code (recommended elsewhere (CloudKit won't reset my badge count to 0):
func resetBadgeCounter() {
let badgeResetOperation = CKModifyBadgeOperation(badgeValue: 0)
badgeResetOperation.modifyBadgeCompletionBlock = { (error) -> Void in
if error != nil {
print("Error resetting badge: \(String(describing: error))")
}
else {
UIApplication.shared.applicationIconBadgeNumber = 0
}
}
CKContainer.default().add(badgeResetOperation)
}
This works for now, but is no longer supported, and may go away soon.
I thought perhaps I should use a CKModfyRecordsOperation or some other CKDatabaseOperation, but I can't even guess how.

I hope this helps someone as there doesn't seem to be another solution/workaround to this.
Create a Notification Service Extension (UNNotificationServiceExtension) for your app. It is fairly straightforward and is described in detail in Modifying Content in Newly Delivered Notifications. You might already have one, if you do any kind of processing on incoming notifications.
Within the extension, you typically do all the processing of the notification in override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void). The request comes with the content of the notification, which includes the badge number. You can then modify that and pass on to the contentHandler to be displayed. (If you are unsure how to do this, check the link above; it has detailed instructions and code.)
To keep track and reset the badge number when needed, you need to take advantage of an app group (e.g. group.com.tzatziki). This way you can share data between the app and the extension. Creating an app group in Xcode can be done by adding the relevant capability and is explained in App Extension Programming Guide: Handling Common Scenarios.
Use a storage mechanism (within the app group) to keep track of the badge count, e.g. UserDefaults, and use it in the extension to update the badge count. In the aforementioned override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void), you could write something like this:
let badgeNumber = UserDefaults(suiteName: "group.com.tzatziki")?.value(forKey: "badgeNumber") as? NSNumber ?? NSNumber(integerLiteral: 0)
let badgeNumberInt = badgeNumber.intValue + 1
notificationContent.badge = NSNumber(value: badgeNumberInt)
UserDefaults(suiteName: "group.io.prata")?.setValue(notificationContent.badge, forKey: "badgeNumber")
where notificationContent is an instance of UNMutableNotificationContent, originating from request.content.mutableCopy() as? UNMutableNotificationContent
Now, the only thing left to do is to reset the badge count in your app, in the shared UserDefaults and in UIApplication.shared.applicationIconBadgeNumber.
Cheers!

It was reported here: https://levelup.gitconnected.com/how-to-reset-badge-value-of-cloudkit-remote-notification-on-ios-ipados-15-46726a435599 that Apple is aware that they need to maintain CKModifyBadgeOperation until they come up with a replacement API. There is no other solution to this problem because CloudKit resets the badge number whenever it pushes a new notification.

It's probably best to just track the items you are counting and set the app badge count yourself. I reference a local database that has my items in it, and I return the total and set my app badge accordingly.

Related

CPQ Quote API, I can't save the quote

I can't save the quote.
Doing the query:
select
ApexClass.name, Id, CreatedDate, CreatedById, JobType,
ApexClassId, Status, JobItemsProcessed, TotalJobItems,
NumberOfErrors, CompletedDate, MethodName, ExtendedStatus,
ParentJobId, LastProcessed, LastProcessedOffset
from
AsyncApexJob
order by
CreatedDate desc
I get this error:
Calculation error on quote Q-13761: "UNAUTHORIZED"
Code:
public with sharing class QuoteCalculator {
public void calculate(QuoteModel quote, String callbackClass) {
system.debug('quote: ' +quote);
system.debug('callbackClass: ' +callbackClass);
QuoteCalculatorContext ctx = new QuoteCalculatorContext(quote, callbackClass);
SBQQ.ServiceRouter.load('SBQQ.QuoteAPI.QuoteCalculator', null, JSON.serialize(ctx));
system.debug('QuoteCalculator.calculate');
}
private class QuoteCalculatorContext {
private QuoteModel quote; //The quote and callbackClass properties are called
in the API code by the exact names seen here.
private String callbackClass; //Altering these property names will cause
calculator API calls to fail.
private QuoteCalculatorContext(QuoteModel quote, String callbackClass) {
this.quote = quote;
this.callbackClass = callbackClass;
}
}
}
anonymous window:
QuoteReader reader = new QuoteReader();
QuoteModel quote = reader.read('a0p1w000BhfXzAAJ');
System.debug(quote);
quote.lineItems[0].record.SBQQ__Quantity__c = 2;
QuoteCalculator calculator = new QuoteCalculator();
calculator.calculate(quote, 'MyCallback')
Preface
I had (almost) the same exact code base as yours, and got the same error message.
In my case there was an other sandbox I could test my code, and it turned out to be working properly there.
Cause
Later found out that the Salesforce CPQ's Calculation Quote API is using Heroku to do the calculations in order to avoid apex limits exhaustion.
From this it can be deducted, that it needs to have a Connected App. I checked the Apps -> Connected Apps setup, and found that no record was listed under the "Connected Apps OAuth Usage" page for the Salesforce CPQ. (On my other sandbox there was a "Steelbrick CPQ" row.)
From this I concluded that this might be the reason for this behaviour.
Seems like something went wrong during the "Authorize new Calculation Service" process. (Or there was a sandbox refresh and something else went wrong during it.)
Solution
The bad news is that the option to authorize a new calculation service is only visible for the first time you configure the package, which you might already done. (Well... if you haven't done, then this is a great news, because your problem is probably solved. :D) (Otherwise read further.)
The good news is I figured out a solution for the case when you already done this, yet that "Steelbrick CPQ" row is missing.
Created a scratch org and installed the Salesforce CPQ package, then before I clicked on the "Authorize new Calculation Service" link under the "Pricing and Calculation" tab in the Settings Editor, I checked the source code in hope of finding something of interest.
I did.
This link: https://rest-na.steelbrick.com/oauth/auth/https%3A%2F%2Ftest.salesforce.com/SBQQ
(⚠️NOTE: You might have to change it according to your location. There are several servers across the globe:
rest-au.steelbrick.com
rest-eu.steelbrick.com
rest-jp.steelbrick.com
rest-na.steelbrick.com
But for me the above pasted link was generated on the settings page. Which is only interesting, because I live in the EU, yet, for some reason I got the link to the rest-NA server... whatever.gif
So just make sure if you click on the link, in the address bar you can find the appropriate salesforce instance URL.)
Conclusion
With this link you won't have to reinstall the package, you just have to click on it, and allow the access from Steelbrick and the missing row will appear, and you will be authorized to use the Calculation API.

Getting All Users Uid from Firebase / Flutter/Dart

My application is kind of a poll application. So the admin asks a question, and users respond to that question, and every data is stored in Firestore. In the admin page, the admin can see the question that he/she created, and when the admin clicks the question, he/she can see the answers of users to this question.
On the answers page, I am trying to reach every user's uid but I couldn't achieve this. I can print them but I can't use them in a way. I think I make some mistakes with Firebase functions.
This is an example of a method that I am trying to return the list includes of the specific question. But this function doesn't return anything.
List getUserIDData() {
List<String> growableList = [];
List<String> userAnswerList = [];
String adminQuestion = "What is the reason of life?";
Firestore.instance.collection("user").getDocuments().then((snapshot) {
snapshot.documents.forEach((element) {
growableList.add(element.documentID);
});
for (int i = 0; i < growableList.length; i++) {
Firestore.instance
.collection("user")
.document(growableList[i])
.collection("answers")
.where("adminQuestion", isEqualTo: adminQuestion)
.getDocuments()
.then((snapshot) {
snapshot.documents.forEach((element) {
userAnswerList.add(element["userAnswer"]);
});
});
print(i);
}
});
return growableList;
}
I also tried something with StreamBuilder, but still, after Firestore.instance, everything is deleted automatically. In Firestore, everything looks good, but after it finished growableList doesn't stay the same and this function returns null.
Do you have any suggestions to do so?
From what I can see you are using a very old version of FlutterFire, for example the following has been changed:
BREAKING: getDocuments/get has been updated to accept an instance of GetOptions (see below).
DEPRECATED: documents has been deprecated in favor of docs.
DEPRECATED: documentID has been deprecated in favor of id.
You might want to migrate to a newer version.
With snapshot.documents you get a List<QueryDocumentSnapshot<T>> according to the documentation. So I looked at the QueryDocumentSnapshot and was surprised that you apply element["userAnswer"] directly to it. According to the documentation you have to use element.data() to get the data.
If that doesn't help I would need more information in the form of output what's behind the element variables.

Verify a url is where it is going Swift

In my app I am trying to have people be able to input links to facebook and youtube. My objective is to make sure that the links they are inputting are actually to facebook & youtube. I'm coming across the issue of figuring out a way to go effectively go about this. Facebook has multiple subdomains, such as "www.facebook.com" & "www.fb.com". Youtube has multiple domains such as "www.youtube.com & "https://youtu.be/". How would I go about verifying that the links they have are actually to either of these subdomains?
what I have so far is this:
let possibleLinks = ["www.facebook.com", "www.fb.com", "www.youtube.com", "https://youtu.be/"]
func verifyLinks(LinkType:String, linkURL:String) {
for url in possibleLinks {
if url != linkURL {
print("This link doesn't match our records of urls - (\(url))")
break
}
}
}
I am struggling to figuring out how to effectively go about this. My biggest issue is due to the fact that lengths of each of the strings are different "fb" is 2 characters, & "facebook" is 6. Is there a way to eliminate "www" ".com" "https"? & if so would there be an issue with the "youtu.be"? B/C there is no .com? Any help would be much appreciated it!
So you have an explicit list of hosts you trust:
let validHosts = ["www.facebook.com", "www.fb.com", "www.youtube.com", "youtu.be"]
And you have an URL you want to check:
let url = URL(string: "https://www.facebook.com/mycontent/is/here")!
Using an URL here is how you avoid all the problems about "https" and the like. Don't use String when you mean URL.
So you just need to make sure the URL's host is in your valid list:
func isValid(url: URL) -> Bool {
guard let host = url.host else { return false }
return validHosts.contains(host)
}
This is the most explicit approach, but it means if someone uses "facebook.com" it's not going to work. You could just include the alternatives you support in your list, and that's by far the most secure approach and what I recommend. It's possible there's some service in facebook.com that could be exploited if you allowed people to link to it (a redirector for example).
That said, it's a useful thing to answer. How would you accept any "fb.com" address like "fb.com", "www.fb.com", and "messenger.fb.com" but not "bobfb.com"? We can do this by breaking up the host into its reverse DNS components and make sure the heads match up to the length of the trusted one.
First, we'd make a helper to return the reverseDNSComponents:
extension String {
var reverseDNSComponents: [String] {
return components(separatedBy: ".").reversed()
}
}
And we'll store our valid list in that format:
let validDomainComponents = ["facebook.com", "fb.com", "youtube.com", "youtu.be"]
.map { $0.reverseDNSComponents }
And now we can test something we're handed:
func isValid(url: URL) -> Bool {
guard let host = url.host else { return false }
let urlDomainComponents = host.reverseDNSComponents
// Make sure that the suffix of the host is in the list of valid domains
return validDomainComponents
.contains { Array(urlDomainComponents.prefix($0.count)) == $0 }
}
(But unless you really need this flexibility, I'd use the explicit approach.)

FBPlacePickerViewController doesn't load any data

I am trying to use FBPlacePickerViewController and it doesn't seem to load any data.
Here is my code:
FBPlacePickerViewController *picker = (FBPlacePickerViewController*)segue.destinationViewController;
picker.navigationController.navigationBarHidden = YES;
picker.delegate = self;
picker.radiusInMeters = 1000;
picker.resultsLimit = 30;
if([TonerAppDelegate instance].lastLocation != nil){
picker.locationCoordinate = [TonerAppDelegate instance].lastLocation.coordinate;
[picker loadData];
}
[TonerAppDelegate instance].lastLocationUpdateFunction = ^{
picker.locationCoordinate = [TonerAppDelegate instance].lastLocation.coordinate;
[picker loadData];
};
It is an embed segue (iOS 6). I verify that the picker is a valid object. The [picker loadData] method does get called, and the coordinate data is perfectly valid. I am not getting any exceptions or warnings. I've allowed my app to access to my location in iOS and I double-verified that in Settings. My iPod is connected to the Internet and the connection works perfectly. All the other apps can use location services without any problem. So, there probably is a problem with my implementation of the place picker. I've also implemented the -(void)placePickerViewControllerDataDidChange:(FBPlacePickerViewController *)placePicker and -(BOOL)placePickerViewController:(FBPlacePickerViewController *)placePicker shouldIncludePlace:(id<FBGraphPlace>) methods of the delegate, and they aren't getting called either. What am I doing wrong?
Thanks,
Can.
Found the answer: I wasn't creating the Facebook session before displaying the place picker. I totally forgot about the session. It'd be nice to see Facebook add an assertion check in loadData method of the picker for an existing Facebook session. I've created the session, and THEN tried my code, and it works perfectly now.

WMS GetFeatureInfo; multiple layers, different sources

I'm developing a web application using GeoExt, OpenLayers and having my own GeoServer to serve various maps. Still, I want to let the user add other WMS's if needed, to be able to play around with all desired layers.
Thus, my problem with the GetFeatureInfo request. Right now I have a toolbar button attached to geoext's map panel,
new GeoExt.Action({
iconCls: "feature",
map: map,
toggleGroup: "tools",
tooltip: "Feature",
control: featureControl
})
its control attribute being
var featureControl = new OpenLayers.Control.WMSGetFeatureInfo({
queryVisible: true,
drillDown: true,
infoFormat:"application/vnd.ogc.gml"
});
I've also defined an event listener to do what I really want once I receive the responses, but that is not relevant here. My problem is the following:
Considering the user clicks on a point where there are 2+ visible layers and at least one of them is from a different source, OpenLayers will have to do one AJAX request per different source and, from OpenLayers own documentation,
Triggered when a GetFeatureInfo response is received. The event
object has a text property with the body of the response (String), a
features property with an array of the parsed features, an xy property
with the position of the mouse click or hover event that triggered the
request, and a request property with the request itself. If drillDown
is set to true and multiple requests were issued to collect feature
info from all layers, text and request will only contain the response
body and request object of the last request.
so, yeah, it will obviously wont work like that right away. Having a look at the debugger I can clearly see that, giving two layers from different sources, it actually DOES the request, it's just that it doesn't wait for the first's response and jumps for the next one (obviously, being asynchronous). I've thought about doing the requests one-by-one, meaning doing the first one as stated above and once it's finished and the response saved, go for the next one. But I'm still getting used to the data structure GeoExt uses.
Is there any API (be it GeoExt or OpenLayers) option/method I'm missing? Any nice workarounds?
Thanks for reading :-)
PS: I'm sorry if I've not been clear enough, english is not my mother tongue. Let me know if something stated above was not clear enough :)
i Hope this help to someone else, I realized that: you're rigth this control make the request in asynchronous mode, but this is ok, no problem with that, the real problem is when the control handle the request and trigger the event "getfeatureinfo" so, i modified 2 methods for this control and it works!, so to do this i declare the control first, and then in the savage mode i modified the methods here is de code:
getInfo = new OpenLayers.Control.WMSGetFeatureInfo({ drillDown:true , queryVisible: true , maxFeatures:100 });
//then i declare a variable that help me to handle more than 1 request.....
getInfo.responses = [];
getInfo.handleResponse=function(xy, request) { var doc = request.responseXML;
if(!doc || !doc.documentElement) { doc = request.responseText; }
var features = this.format.read(doc);
if (this.drillDown === false) {
this.triggerGetFeatureInfo(request, xy, features);
} else {
this._requestCount++;
this._features = (this._features || []).concat(features);
if( this._numRequests > 1){
//if the num of RQ, (I mean more than 1 resource ), i put the Request in array, this is for maybe in a future i could be need other properties or methods from RQ, i dont know.
this.responses.push(request);}
else{
this.responses = request;}
if (this._requestCount === this._numRequests) {
//here i change the code....
//this.triggerGetFeatureInfo(request, xy, this._features.concat());
this.triggerGetFeatureInfo(this.responses, xy, this._features.concat());
delete this._features;
delete this._requestCount;
delete this._numRequests;
// I Adding this when the all info is done 4 reboot
this.responses=[];
}
}
}
getInfo.triggerGetFeatureInfo= function( request , xy , features) {
//finally i added this code for get all request.responseText's
if( isArray( request ) ){
text_rq = '';
for(i in request ){
text_rq += request[i].responseText;
}
}
else{
text_rq = request.responseText;
}
this.events.triggerEvent("getfeatureinfo", {
//text: request.responseText,
text : text_rq,
features: features,
request: request,
xy: xy
});
// Reset the cursor.
OpenLayers.Element.removeClass(this.map.viewPortDiv, "olCursorWait");}
Thanks, you bring me a way for discover my problem and here is the way i solved, i hope this can help to somebody else.
saheka's answer was almost perfect! Congratulations and thank you, I had the same problem, and with it I finally managed to solve it.
What I would change in your code:
isArray() does not work, change it like this: if(request instanceof Array) {...} at the first line of getInfo.triggerGetFeatureInfo()
to show the results in a popup this is the way:
My code:
getInfo.addPopup = function(map, text, xy) {
if(map.popups.length > 0) {
map.removePopup(map.popups[0]);
}
var popup = new OpenLayers.Popup.FramedCloud(
"anything",
map.getLonLatFromPixel(xy),
null,
text,
null,
true
);
map.addPopup(popup);
}
and in the getInfo.triggerGetFeatureInfo() function, after the last line, append:
this.addPopup(map, text_rq, xy);
A GetFeatureInfo request is send as a JavaScript Ajax call to the external server. So, the requests are likely blocked for security reasons. You'll have to send the requests to the external servers by a proxy on your own domain.
Then, configure this proxy in openlayers by setting OpenLayers.ProxyHost to the proper path. For example:
OpenLayers.ProxyHost = "/proxy_script";
See http://trac.osgeo.org/openlayers/wiki/FrequentlyAskedQuestions#ProxyHost for more background information.

Resources